@styleflow.app/flowpress-deploy 1.0.0-beta.1 → 1.0.0-beta.11

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 (103) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +6 -29
  3. package/containers/cms/Dockerfile +35 -0
  4. package/containers/cms/Dockerfile.dockerignore +20 -0
  5. package/containers/cms/healthz.php +17 -0
  6. package/containers/cms/install-languages.sh +16 -0
  7. package/containers/cms/mpm.conf +10 -0
  8. package/containers/cms/php.ini +16 -0
  9. package/containers/cms/sanitize.php +37 -0
  10. package/containers/cms/smtp.php +13 -0
  11. package/containers/cms/vhost.conf +61 -0
  12. package/containers/cms/wp-cli.yml +1 -0
  13. package/containers/php-runtime/Dockerfile +21 -0
  14. package/containers/web-ssr/Dockerfile +25 -0
  15. package/containers/web-ssr/Dockerfile.dockerignore +10 -0
  16. package/containers/web-static/Caddyfile +40 -0
  17. package/containers/web-static/Dockerfile +15 -0
  18. package/containers/web-static/Dockerfile.dockerignore +7 -0
  19. package/dist/frontend-assets.d.ts +2 -0
  20. package/dist/frontend-assets.d.ts.map +1 -0
  21. package/dist/frontend-assets.js +44 -0
  22. package/dist/frontend-assets.js.map +1 -0
  23. package/dist/frontend-links.d.ts +14 -0
  24. package/dist/frontend-links.d.ts.map +1 -0
  25. package/dist/frontend-links.js +82 -0
  26. package/dist/frontend-links.js.map +1 -0
  27. package/dist/ops/bootstrap.d.ts +21 -0
  28. package/dist/ops/bootstrap.d.ts.map +1 -0
  29. package/dist/ops/bootstrap.js +123 -0
  30. package/dist/ops/bootstrap.js.map +1 -0
  31. package/dist/ops/build.d.ts +39 -0
  32. package/dist/ops/build.d.ts.map +1 -0
  33. package/dist/ops/build.js +108 -0
  34. package/dist/ops/build.js.map +1 -0
  35. package/dist/ops/cli.d.ts +3 -0
  36. package/dist/ops/cli.d.ts.map +1 -0
  37. package/dist/ops/cli.js +140 -0
  38. package/dist/ops/cli.js.map +1 -0
  39. package/dist/ops/config.d.ts +54 -0
  40. package/dist/ops/config.d.ts.map +1 -0
  41. package/dist/ops/config.js +177 -0
  42. package/dist/ops/config.js.map +1 -0
  43. package/dist/ops/coolify.d.ts +2 -0
  44. package/dist/ops/coolify.d.ts.map +1 -0
  45. package/dist/ops/coolify.js +44 -0
  46. package/dist/ops/coolify.js.map +1 -0
  47. package/dist/ops/data.d.ts +27 -0
  48. package/dist/ops/data.d.ts.map +1 -0
  49. package/dist/ops/data.js +148 -0
  50. package/dist/ops/data.js.map +1 -0
  51. package/dist/ops/deploy.d.ts +18 -0
  52. package/dist/ops/deploy.d.ts.map +1 -0
  53. package/dist/ops/deploy.js +100 -0
  54. package/dist/ops/deploy.js.map +1 -0
  55. package/dist/ops/editorial.d.ts +23 -0
  56. package/dist/ops/editorial.d.ts.map +1 -0
  57. package/dist/ops/editorial.js +79 -0
  58. package/dist/ops/editorial.js.map +1 -0
  59. package/dist/ops/environment.d.ts +29 -0
  60. package/dist/ops/environment.d.ts.map +1 -0
  61. package/dist/ops/environment.js +159 -0
  62. package/dist/ops/environment.js.map +1 -0
  63. package/dist/ops/health.d.ts +25 -0
  64. package/dist/ops/health.d.ts.map +1 -0
  65. package/dist/ops/health.js +22 -0
  66. package/dist/ops/health.js.map +1 -0
  67. package/dist/ops/monitor.d.ts +23 -0
  68. package/dist/ops/monitor.d.ts.map +1 -0
  69. package/dist/ops/monitor.js +143 -0
  70. package/dist/ops/monitor.js.map +1 -0
  71. package/dist/ops/process.d.ts +9 -0
  72. package/dist/ops/process.d.ts.map +1 -0
  73. package/dist/ops/process.js +48 -0
  74. package/dist/ops/process.js.map +1 -0
  75. package/dist/ops/provision.d.ts +41 -0
  76. package/dist/ops/provision.d.ts.map +1 -0
  77. package/dist/ops/provision.js +262 -0
  78. package/dist/ops/provision.js.map +1 -0
  79. package/dist/ops/registry.d.ts +2 -0
  80. package/dist/ops/registry.d.ts.map +1 -0
  81. package/dist/ops/registry.js +36 -0
  82. package/dist/ops/registry.js.map +1 -0
  83. package/dist/ops/release.d.ts +26 -0
  84. package/dist/ops/release.d.ts.map +1 -0
  85. package/dist/ops/release.js +79 -0
  86. package/dist/ops/release.js.map +1 -0
  87. package/dist/ops/s3.d.ts +8 -0
  88. package/dist/ops/s3.d.ts.map +1 -0
  89. package/dist/ops/s3.js +38 -0
  90. package/dist/ops/s3.js.map +1 -0
  91. package/dist/ops/static-config.d.ts +14 -0
  92. package/dist/ops/static-config.d.ts.map +1 -0
  93. package/dist/ops/static-config.js +96 -0
  94. package/dist/ops/static-config.js.map +1 -0
  95. package/dist/worker-assets.d.ts +5 -0
  96. package/dist/worker-assets.d.ts.map +1 -0
  97. package/dist/worker-assets.js +35 -0
  98. package/dist/worker-assets.js.map +1 -0
  99. package/flowpress-deploy.php +3 -2
  100. package/includes/automatic-build.php +98 -0
  101. package/includes/queue-provider.php +2 -0
  102. package/package.json +50 -7
  103. package/schema/deployment.schema.json +45 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.11
4
+
5
+ - Resolve platform server and panel references from the canonical infrastructure inventory.
6
+ - Reuse an existing CMS image by commit SHA; refuse ambiguous registry failures instead of overwriting a release.
7
+ - Verify status against the image configured in Coolify after CI deployments.
8
+
9
+
10
+ ## 1.0.0-beta.10
11
+
12
+ - Keep local HTTP origins out of remote WordPress frontend registries.
13
+ - Read the shared engine administrator secret from the operator configuration; never silently replace a missing pool password.
14
+
15
+
16
+ ## 1.0.0-beta.9
17
+
18
+ - Keep editorial pages under /media/ available in static deployments; proxy only media files. Verify the behavior against a real Caddy runtime.
19
+
20
+
21
+ ## 1.0.0-beta.8
22
+
23
+ - Exclude private backup files from media snapshots and reject them before uploads. Deny HTTP access to SQL dumps and migration backup directories.
24
+ - Preserve existing schema backup bindings across repeated provisioning.
25
+
26
+
27
+ ## 1.0.0-beta.7
28
+
29
+ - Preserve the local backend selection and explicit environment overrides used by isolated build fixtures. Stored environment URLs still defer to the manifest.
30
+
31
+
32
+ ## 1.0.0-beta.6
33
+
34
+ - Encode MariaDB configuration for the Coolify API and preserve robot level when extending Harbor pull permissions.
35
+ - Retain current and previous deployment aliases without changing immutable release tags.
36
+
37
+
38
+ ## 1.0.0-beta.5 — 2026-10-19
39
+
40
+ - Use individually scoped system robots for repeatable Harbor provisioning; pass content build IDs to SSR health checks.
41
+ - [Canonical report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-19-piattaforma-container-flowpress.md).
42
+
43
+ ## 1.0.0-beta.4 — 2026-10-19
44
+
45
+ - Add published FlowPress Ops CLI, readable deployment manifests, shared MariaDB bindings, reusable container runtimes, isolated environment loading, immutable releases, backup verification and on-demand preview.
46
+ - [Canonical report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-19-piattaforma-container-flowpress.md).
47
+
48
+ ## Unreleased
49
+
50
+ - Agent rules only, no runtime or API change: `AGENTS.md` names the canonical
51
+ docs vault path (2026-10-18) and requires every published change to be
52
+ merged into `main`, with its branches, worktrees and stashes removed in the
53
+ same task.
54
+ [Canonical report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-19-branch-residui-e-regole-di-chiusura.md).
55
+
56
+ ## 1.0.0-beta.3 — 2026-10-14
57
+
58
+ - Queue complete builds for term-only changes to published content, including withdrawal after a visibility change. Keep unchanged assignments idle and avoid private data in the queue.
59
+ - Verified public → private → public on disposable WordPress with all outbound HTTP intercepted.
60
+ - [Canonical integration report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-14-consolidamento-tessari.md).
61
+
62
+ ## 1.0.0-beta.2 — 2026-10-14
63
+
64
+ - Automatic debounced editorial builds and public asset validation/history.
65
+ - Use public taxonomy hooks for scheduled builds and export portable link, asset-history and Worker validation tools.
66
+ - [Canonical integration report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-14-consolidamento-tessari.md).
67
+
68
+
3
69
  ## 1.0.0-beta.1
4
70
 
5
71
  - Added the automatically selected `flowpress-deploy` Core queue provider.
package/README.md CHANGED
@@ -1,34 +1,11 @@
1
1
  # FlowPress Deploy
2
2
 
3
- Installable npm/Composer feature pack for revision-safe editorial queues and
4
- safe full/partial build orchestration. WordPress owns a durable queue per
5
- frontend target; frontend workflows keep the existing `batchId`, `target`,
6
- `mode`, `paths` and `deletedPaths` wire contract.
3
+ Official npm/Composer integration for FlowPress.
7
4
 
8
- The public API validates paths and `_routes.json`, merges partial artifacts
9
- without traversal or case collisions, regenerates sitemaps, selects Pages for
10
- static targets and Workers for dev, requires `ASTRO_KEY` for protected batches,
11
- and acknowledges the CMS queue only after deploy and smoke test. Failed
12
- batches are released for retry.
5
+ - [Canonical guide](https://github.com/StyleFlow-App/styleflow-docs/blob/main/wiki/flowpress-routing-deploy.md)
6
+ - [Release and verification report](https://github.com/StyleFlow-App/styleflow-docs/blob/main/dev/rapporti/2026-10-14-consolidamento-tessari.md)
13
7
 
14
- ## WordPress queue
8
+ - [Container platform and FlowPress Ops](https://github.com/StyleFlow-App/styleflow-docs/blob/main/wiki/piattaforma-container-flowpress.md)
15
9
 
16
- When the Composer package is installed, Deploy automatically becomes the
17
- FlowPress Core queue provider. It fingerprints content at the end of the
18
- request, groups WPML translations, keeps current and previous URIs and stores
19
- a revision snapshot for every dispatched batch. A callback acknowledges only
20
- the dispatched revision; concurrent editorial changes remain pending.
21
-
22
- Editors can use **Aggiungi alla build queue** from edit screens and list rows.
23
- Administrators operate the queue in **Strumenti → FlowPress Deploy**.
24
-
25
- ## Documentation
26
-
27
- Start with [docs/README.md](docs/README.md). It links configuration, queue
28
- semantics, frontend integration, operations, recovery and troubleshooting.
29
-
30
- ## WordPress Core v1
31
-
32
- The `1.0.0-beta.x` line requires FlowPress WordPress Core v1. It does not
33
- provide a v0.x compatibility constraint or adapter; canary applications pin
34
- the selected Core prerelease exactly.
10
+ The npm package also provides the flowpress-ops binary, reusable container
11
+ recipes and the ./ops/environment API. Run flowpress-ops --help for commands.
@@ -0,0 +1,35 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # The reusable PHP runtime is built once by platform build-base.
3
+ ARG FLOWPRESS_PHP_IMAGE
4
+ FROM $FLOWPRESS_PHP_IMAGE AS build
5
+ USER root
6
+ RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
7
+ COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
8
+ WORKDIR /srv/cms
9
+ COPY apps/cms/ ./
10
+ RUN --mount=type=secret,id=composer_auth,required=true \
11
+ --mount=type=cache,target=/root/.composer/cache \
12
+ COMPOSER_AUTH="$(cat /run/secrets/composer_auth)" \
13
+ composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-progress
14
+ COPY --from=flowpress /cms/install-languages.sh /usr/local/bin/install-languages
15
+ RUN sh /usr/local/bin/install-languages /srv/cms/web
16
+ ARG WP_CLI_VERSION=2.12.0
17
+ ARG WP_CLI_SHA256=ce34ddd838f7351d6759068d09793f26755463b4a4610a5a5c0a97b68220d85c
18
+ RUN curl -fsSL "https://github.com/wp-cli/wp-cli/releases/download/v$WP_CLI_VERSION/wp-cli-$WP_CLI_VERSION.phar" -o /wp-cli.phar \
19
+ && echo "$WP_CLI_SHA256 /wp-cli.phar" | sha256sum -c - && chmod 755 /wp-cli.phar
20
+ FROM $FLOWPRESS_PHP_IMAGE
21
+ WORKDIR /srv/cms
22
+ COPY --from=build /srv/cms/ ./
23
+ COPY --from=build /wp-cli.phar /usr/local/bin/wp
24
+ COPY --from=flowpress /cms/wp-cli.yml /srv/cms/wp-cli.yml
25
+ COPY --from=flowpress /cms/healthz.php /srv/cms/web/healthz.php
26
+ COPY --from=flowpress /cms/smtp.php /usr/local/share/flowpress-smtp.php
27
+ RUN mkdir -p web/app/uploads web/app/webp-express web/app/cache web/app/upgrade \
28
+ && chown -R www-data:www-data web/app/uploads web/app/webp-express web/app/cache web/app/upgrade \
29
+ && echo 'auto_prepend_file=/usr/local/share/flowpress-smtp.php' > /usr/local/etc/php/conf.d/zz-smtp.ini
30
+ ARG RELEASE_SHA=unknown
31
+ ENV RELEASE_SHA=$RELEASE_SHA
32
+ LABEL org.opencontainers.image.revision=$RELEASE_SHA
33
+ USER www-data
34
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 CMD curl -fsS http://127.0.0.1:8080/healthz.php > /dev/null || exit 1
35
+ CMD ["apache2-foreground"]
@@ -0,0 +1,20 @@
1
+ .git
2
+ node_modules
3
+ .env*
4
+ .secrets
5
+ .local
6
+ .styleflow
7
+ apps/web
8
+ packages
9
+ docs
10
+ apps/cms/vendor
11
+ apps/cms/auth.json
12
+ apps/cms/.env*
13
+ apps/cms/web/wp
14
+ apps/cms/web/app/plugins
15
+ apps/cms/web/app/uploads
16
+ apps/cms/web/app/webp-express
17
+ apps/cms/web/app/cache
18
+ apps/cms/web/app/upgrade
19
+ apps/cms/web/app/mu-plugins/flowpress-*
20
+ apps/cms/web/app/mu-plugins/flowpress-core
@@ -0,0 +1,17 @@
1
+ <?php
2
+ declare(strict_types=1);
3
+ header('Content-Type: application/json');
4
+ header('Cache-Control: no-store');
5
+ try {
6
+ define('SHORTINIT', true);
7
+ require __DIR__ . '/wp/wp-load.php';
8
+ global $wpdb;
9
+ $installed = $wpdb->get_var("SELECT option_value FROM {$wpdb->options} WHERE option_name = 'siteurl'");
10
+ if (!$installed || !is_readable(__DIR__ . '/app/mu-plugins/flowpress-core/flowpress-core.php')) {
11
+ throw new RuntimeException('CMS not initialized.');
12
+ }
13
+ echo json_encode(['status' => 'ok', 'environment' => getenv('APP_ENV'), 'release' => getenv('RELEASE_SHA')]);
14
+ } catch (Throwable $error) {
15
+ http_response_code(503);
16
+ echo json_encode(['status' => 'error', 'release' => getenv('RELEASE_SHA')]);
17
+ }
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # Installs the Italian WordPress core translation matching the installed core.
3
+ # Languages are gitignored and DISALLOW_FILE_MODS blocks installing them at
4
+ # runtime, so they are part of the image.
5
+ set -eu
6
+ web_root="$1"
7
+ version="$(php -r "include '$web_root/wp/wp-includes/version.php'; echo \$wp_version;")"
8
+ package="$(curl -fsSL "https://api.wordpress.org/translations/core/1.0/?version=$version" \
9
+ | php -r '$d = json_decode(stream_get_contents(STDIN), true);
10
+ foreach ($d["translations"] ?? [] as $t) { if ($t["language"] === "it_IT") { echo $t["package"]; exit; } }
11
+ exit(1);')"
12
+ mkdir -p "$web_root/app/languages"
13
+ curl -fsSL "$package" -o /tmp/it_IT.zip
14
+ unzip -oq /tmp/it_IT.zip -d "$web_root/app/languages"
15
+ rm /tmp/it_IT.zip
16
+ echo "Installed it_IT core translation for WordPress $version."
@@ -0,0 +1,10 @@
1
+ # Sized for a 1536m container limit: 8 workers at up to 256M PHP memory each,
2
+ # typical WordPress requests use far less.
3
+ <IfModule mpm_prefork_module>
4
+ StartServers 2
5
+ MinSpareServers 2
6
+ MaxSpareServers 4
7
+ ServerLimit 8
8
+ MaxRequestWorkers 8
9
+ MaxConnectionsPerChild 500
10
+ </IfModule>
@@ -0,0 +1,16 @@
1
+ expose_php = Off
2
+ display_errors = Off
3
+ log_errors = On
4
+ error_log = /proc/self/fd/2
5
+ memory_limit = 256M
6
+ upload_max_filesize = 64M
7
+ post_max_size = 68M
8
+ max_execution_time = 120
9
+ max_input_vars = 3000
10
+ session.save_path = /tmp
11
+ ; Code is immutable inside the image: never stat files for changes.
12
+ opcache.enable = 1
13
+ opcache.validate_timestamps = 0
14
+ opcache.memory_consumption = 128
15
+ opcache.interned_strings_buffer = 16
16
+ opcache.max_accelerated_files = 20000
@@ -0,0 +1,37 @@
1
+ <?php
2
+ // Executed only against a disposable copy, never against the source.
3
+ declare(strict_types=1);
4
+ if (getenv('FLOWPRESS_RUNTIME_POLICY') !== 'container' || getenv('APP_ENV') === 'production') {
5
+ throw new RuntimeException('Refusing to sanitize a production database.');
6
+ }
7
+ global $wpdb;
8
+ foreach (get_users(['fields' => 'ID']) as $user_id) {
9
+ $wpdb->update($wpdb->users, ['user_pass' => wp_hash_password(wp_generate_password(64, true, true)), 'user_activation_key' => ''], ['ID' => $user_id]);
10
+ delete_user_meta((int) $user_id, 'session_tokens');
11
+ delete_user_meta((int) $user_id, '_application_passwords');
12
+ }
13
+ $plugins = get_option('active_plugins', []);
14
+ update_option('active_plugins', array_values(array_filter($plugins, static fn (string $plugin): bool
15
+ => !preg_match('~^(advanced-media-offloader|wp-mail-smtp|wp-migrate-db-pro)/~', $plugin))));
16
+ foreach (['advmo_settings', 'wp_mail_smtp', 'cron'] as $option) {
17
+ delete_option($option);
18
+ }
19
+ update_option('home', getenv('WP_HOME'));
20
+ update_option('siteurl', getenv('WP_SITEURL'));
21
+ update_option('blog_public', '0');
22
+ $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'flowpress%queue%' OR option_name LIKE '%_transient_%'");
23
+ $attachments = get_posts(['post_type' => 'attachment', 'post_status' => 'any', 'numberposts' => -1, 'fields' => 'ids']);
24
+ foreach ($attachments as $id) {
25
+ $relative = get_post_meta($id, '_wp_attached_file', true);
26
+ if (is_string($relative) && $relative !== '' && !str_contains($relative, '..') && !str_contains($relative, '://')) {
27
+ $wpdb->update($wpdb->posts, ['guid' => rtrim(getenv('WP_HOME'), '/') . '/app/uploads/' . ltrim($relative, '/')], ['ID' => $id]);
28
+ }
29
+ foreach (array_keys(get_post_meta($id)) as $key) {
30
+ if (preg_match('/advmo|offload/i', $key)) delete_post_meta($id, $key);
31
+ }
32
+ }
33
+ echo json_encode([
34
+ 'tables' => count($wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($wpdb->prefix) . '%'))),
35
+ 'published' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish'"),
36
+ 'attachments' => count($attachments),
37
+ ]) . "\n";
@@ -0,0 +1,13 @@
1
+ <?php
2
+ // Runs before Bedrock without reading any project file or persisted option.
3
+ if (getenv('FLOWPRESS_RUNTIME_POLICY') === 'container' && getenv('FLOWPRESS_MAIL_MODE') === 'smtp' && getenv('APP_ENV') === 'production') {
4
+ foreach ([
5
+ 'WPMS_ON' => true, 'WPMS_MAILER' => 'smtp',
6
+ 'WPMS_SMTP_HOST' => getenv('SMTP_HOST'), 'WPMS_SMTP_PORT' => (int) getenv('SMTP_PORT'),
7
+ 'WPMS_SMTP_SSL' => getenv('SMTP_ENCRYPTION'), 'WPMS_SMTP_AUTH' => true,
8
+ 'WPMS_SMTP_USER' => getenv('SMTP_USER'), 'WPMS_SMTP_PASS' => getenv('SMTP_PASSWORD'),
9
+ 'WPMS_MAIL_FROM' => getenv('SMTP_FROM'), 'WPMS_MAIL_FROM_NAME' => getenv('SMTP_FROM_NAME'),
10
+ ] as $key => $value) {
11
+ if (!defined($key)) define($key, $value);
12
+ }
13
+ }
@@ -0,0 +1,61 @@
1
+ # Apache vhost for the container CMS. .htaccess files are ignored
2
+ # (AllowOverride None): every rule WordPress needs is declared here.
3
+ <VirtualHost *:8080>
4
+ ServerName localhost
5
+ DocumentRoot /srv/cms/web
6
+
7
+ <Directory /srv/cms/web>
8
+ AllowOverride None
9
+ Options +FollowSymLinks -Indexes
10
+ Require all granted
11
+ DirectoryIndex index.php
12
+
13
+ RewriteEngine On
14
+ # Application Passwords (cms:template) and Bearer callbacks need the
15
+ # Authorization header inside PHP.
16
+ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
17
+ RewriteRule ^index\.php$ - [L]
18
+ RewriteCond %{REQUEST_FILENAME} !-f
19
+ RewriteCond %{REQUEST_FILENAME} !-d
20
+ RewriteRule . /index.php [L]
21
+ </Directory>
22
+
23
+ # Uploaded files are data, never code.
24
+ <Directory /srv/cms/web/app/uploads>
25
+ <FilesMatch "(?i)\.(php[0-9]?|phtml|phar|pht|sql|sql\.gz|env|bak)$">
26
+ Require all denied
27
+ </FilesMatch>
28
+ </Directory>
29
+ <Directory /srv/cms/web/app/uploads/wp-migrate-db>
30
+ Require all denied
31
+ </Directory>
32
+ <FilesMatch "^\.|^(auth\.json|composer\.(json|lock)|wp-cli\.yml)$">
33
+ Require all denied
34
+ </FilesMatch>
35
+ <Files "xmlrpc.php">
36
+ Require all denied
37
+ </Files>
38
+
39
+ # Traefik (Docker network) sits in front; Cloudflare adds CF-Connecting-IP.
40
+ # Only the private proxy hop is trusted, see docs/wiki/deploy/07-sicurezza-e-accessi.md.
41
+ RemoteIPHeader CF-Connecting-IP
42
+ RemoteIPInternalProxy 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
43
+ SetEnvIfNoCase X-Forwarded-Proto "^https$" HTTPS=on
44
+
45
+ <IfModule mod_expires.c>
46
+ ExpiresActive On
47
+ ExpiresByType image/webp "access plus 30 days"
48
+ ExpiresByType image/avif "access plus 30 days"
49
+ ExpiresByType image/jpeg "access plus 30 days"
50
+ ExpiresByType image/png "access plus 30 days"
51
+ ExpiresByType image/svg+xml "access plus 30 days"
52
+ ExpiresByType font/woff2 "access plus 1 year"
53
+ </IfModule>
54
+
55
+ Header always set X-Content-Type-Options "nosniff"
56
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
57
+
58
+ LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %D" container
59
+ ErrorLog /proc/self/fd/2
60
+ CustomLog /proc/self/fd/1 container
61
+ </VirtualHost>
@@ -0,0 +1 @@
1
+ path: web/wp
@@ -0,0 +1,21 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM php:8.3-apache-bookworm
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ ca-certificates curl default-mysql-client libicu-dev libjpeg62-turbo-dev \
5
+ libpng-dev libwebp-dev libzip-dev unzip webp \
6
+ && docker-php-ext-configure gd --with-jpeg --with-webp \
7
+ && docker-php-ext-install exif gd intl mysqli opcache pdo_mysql zip \
8
+ && a2enmod expires headers remoteip rewrite \
9
+ && rm -rf /var/lib/apt/lists/*
10
+ COPY cms/vhost.conf /etc/apache2/sites-available/000-default.conf
11
+ COPY cms/mpm.conf /etc/apache2/mods-available/mpm_prefork.conf
12
+ COPY cms/php.ini /usr/local/etc/php/conf.d/zz-container.ini
13
+ RUN sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf \
14
+ && sed -i '/Listen 443/d' /etc/apache2/ports.conf \
15
+ && echo 'ServerName localhost' > /etc/apache2/conf-available/servername.conf \
16
+ && a2enconf servername \
17
+ && mkdir -p /tmp/apache2 \
18
+ && chown www-data:www-data /tmp/apache2 \
19
+ && find / -xdev -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null
20
+ ENV APACHE_RUN_DIR=/tmp/apache2 APACHE_PID_FILE=/tmp/apache2/apache2.pid APACHE_LOCK_DIR=/tmp/apache2
21
+ EXPOSE 8080
@@ -0,0 +1,25 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-bookworm-slim AS dependencies
3
+ RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
4
+ WORKDIR /workspace
5
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
6
+ COPY apps/web/package.json apps/web/package.json
7
+ COPY apps/cms/package.json apps/cms/package.json
8
+ COPY packages/ui packages/ui
9
+ ARG WEB_PACKAGE
10
+ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm --filter $WEB_PACKAGE --prod deploy /deploy
11
+ FROM node:22-bookworm-slim
12
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
13
+ && rm -rf /var/lib/apt/lists/* \
14
+ && find / -xdev -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null
15
+ WORKDIR /srv/web
16
+ COPY --from=dependencies /deploy/node_modules ./node_modules
17
+ ARG DIST_DIR
18
+ COPY $DIST_DIR/ ./dist/
19
+ ARG RELEASE_SHA=unknown
20
+ ENV NODE_ENV=production HOST=0.0.0.0 PORT=8080 RELEASE_SHA=$RELEASE_SHA
21
+ LABEL org.opencontainers.image.revision=$RELEASE_SHA
22
+ USER node
23
+ EXPOSE 8080
24
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD curl -fsS http://127.0.0.1:8080/healthz.json > /dev/null || exit 1
25
+ CMD ["node", "dist/server/entry.mjs"]
@@ -0,0 +1,10 @@
1
+ .git
2
+ node_modules
3
+ .env*
4
+ .secrets
5
+ .local
6
+ apps/cms/vendor
7
+ apps/cms/auth.json
8
+ apps/cms/.env*
9
+ apps/cms/web
10
+ docs
@@ -0,0 +1,40 @@
1
+ # Static production frontend. Cloudflare terminates TLS and caches at the edge;
2
+ # Traefik forwards plain HTTP to port 8080.
3
+ {
4
+ admin off
5
+ auto_https off
6
+ persist_config off
7
+ servers {
8
+ trusted_proxies static private_ranges
9
+ }
10
+ }
11
+
12
+ :8080 {
13
+ header X-Content-Type-Options nosniff
14
+ header Referrer-Policy strict-origin-when-cross-origin
15
+ @internal path /.container* /.flowpress* /.env* /server/*
16
+ respond @internal 404
17
+ root * /srv/site
18
+ encode zstd gzip
19
+
20
+ # Redirects, headers and media proxies generated from the Astro build.
21
+ import /srv/site/.container/routes.caddy
22
+
23
+ header /healthz.json Cache-Control "no-store"
24
+ header /_astro/* Cache-Control "public, max-age=31536000, immutable"
25
+ # HTML revalidates in browsers; Cloudflare keeps it at the edge until the
26
+ # deploy purge (see docs/wiki/deploy/06-cloudflare.md).
27
+ @html path / *.html */
28
+ header @html Cache-Control "public, max-age=0, must-revalidate"
29
+
30
+ file_server {
31
+ hide .container .flowpress-baseline.json .flowpress-fingerprint _redirects _headers _routes.json
32
+ }
33
+
34
+ handle_errors 404 {
35
+ header Cache-Control "no-store"
36
+ header CDN-Cache-Control "no-store"
37
+ rewrite * /404.html
38
+ file_server
39
+ }
40
+ }
@@ -0,0 +1,15 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM caddy:2.10-alpine
3
+ RUN apk add --no-cache libcap && setcap -r /usr/bin/caddy && apk del libcap \
4
+ && find / -xdev -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null
5
+ COPY --from=flowpress /web-static/Caddyfile /etc/caddy/Caddyfile
6
+ ARG DIST_DIR
7
+ COPY $DIST_DIR/ /srv/site/
8
+ RUN test -f /srv/site/.container/routes.caddy && caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
9
+ ARG RELEASE_SHA=unknown
10
+ ENV RELEASE_SHA=$RELEASE_SHA
11
+ LABEL org.opencontainers.image.revision=$RELEASE_SHA
12
+ USER 65534:65534
13
+ EXPOSE 8080
14
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz.json || exit 1
15
+ CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
@@ -0,0 +1,7 @@
1
+ .git
2
+ node_modules
3
+ .env*
4
+ .secrets
5
+ .local
6
+ apps/cms
7
+ docs
@@ -0,0 +1,2 @@
1
+ export function retainFrontendAssets(output: any, baselineDirectories: any): Promise<number>;
2
+ //# sourceMappingURL=frontend-assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontend-assets.d.ts","sourceRoot":"","sources":["../src/frontend-assets.js"],"names":[],"mappings":"AAUA,6FA4BC"}
@@ -0,0 +1,44 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { frontendFiles } from "./frontend-links.js";
5
+ const manifestName = ".flowpress-asset-generations.json";
6
+ const clientDirectory = output => existsSync(resolve(output, "client")) ? resolve(output, "client") : output;
7
+ // Keep three distinct sets of immutable assets so pages opened before a deploy
8
+ // can still load their styles, scripts and deferred imports. Never retain HTML.
9
+ export async function retainFrontendAssets(output, baselineDirectories) {
10
+ const client = clientDirectory(output);
11
+ const current = (await frontendFiles(client)).filter(path => path.startsWith("/_astro/")).sort();
12
+ const generations = [current];
13
+ const sources = new Map();
14
+ for (const baseline of baselineDirectories.filter(existsSync)) {
15
+ const baselineClient = clientDirectory(baseline);
16
+ const previousFiles = (await frontendFiles(baselineClient)).filter(path => path.startsWith("/_astro/"));
17
+ for (const path of previousFiles)
18
+ if (!sources.has(path))
19
+ sources.set(path, resolve(baselineClient, path.slice(1)));
20
+ const manifest = await readFile(resolve(baseline, manifestName), "utf8").then(JSON.parse).catch(() => null);
21
+ for (const generation of manifest?.generations ?? [previousFiles.sort()]) {
22
+ if (!generations.some(existing => JSON.stringify(existing) === JSON.stringify(generation)))
23
+ generations.push(generation);
24
+ }
25
+ }
26
+ const retained = generations.slice(0, 3);
27
+ let copied = 0;
28
+ for (const path of new Set(retained.flat())) {
29
+ if (!/^\/_astro\/[A-Za-z0-9_./@-]+$/.test(path) || path.split("/").some(part => part === ".." || part === "."))
30
+ throw new Error(`Invalid retained asset: ${path}`);
31
+ const destination = resolve(client, path.slice(1));
32
+ if (existsSync(destination))
33
+ continue;
34
+ const source = sources.get(path);
35
+ if (!source)
36
+ throw new Error(`Baseline is missing retained asset ${path}`);
37
+ await mkdir(dirname(destination), { recursive: true });
38
+ await copyFile(source, destination);
39
+ copied++;
40
+ }
41
+ await writeFile(resolve(output, manifestName), `${JSON.stringify({ generations: retained }, null, 2)}\n`);
42
+ return copied;
43
+ }
44
+ //# sourceMappingURL=frontend-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontend-assets.js","sourceRoot":"","sources":["../src/frontend-assets.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export function frontendFiles(directory: any): Promise<any[]>;
2
+ export function htmlReferences(html: any): any[];
3
+ export function inspectFrontendLinks(directory: any, { origins, workerPatterns }?: {
4
+ origins?: never[] | undefined;
5
+ workerPatterns?: never[] | undefined;
6
+ }): Promise<{
7
+ paths: string[];
8
+ broken: {
9
+ source: any;
10
+ path: string;
11
+ }[];
12
+ pages: number;
13
+ }>;
14
+ //# sourceMappingURL=frontend-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontend-links.d.ts","sourceRoot":"","sources":["../src/frontend-links.js"],"names":[],"mappings":"AAIA,8DAWC;AAED,iDAWC;AAED;;;;;;;;;;GAwCC"}
@@ -0,0 +1,82 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { invokesWorkerFirst } from "./worker-assets.js";
4
+ export async function frontendFiles(directory) {
5
+ const files = [];
6
+ async function visit(current) {
7
+ for (const entry of await readdir(current, { withFileTypes: true })) {
8
+ const absolute = resolve(current, entry.name);
9
+ if (entry.isDirectory())
10
+ await visit(absolute);
11
+ if (entry.isFile())
12
+ files.push(`/${relative(directory, absolute).split(sep).join("/")}`);
13
+ }
14
+ }
15
+ await visit(directory);
16
+ return files;
17
+ }
18
+ export function htmlReferences(html) {
19
+ const references = [];
20
+ for (const tag of html.matchAll(/<(?:a|link|script|img|source|video|audio|iframe|form)\b[^>]*>/gi)) {
21
+ for (const attribute of tag[0].matchAll(/\s(href|src|poster|action|srcset)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) {
22
+ const value = (attribute[2] ?? attribute[3]).replaceAll("&amp;", "&");
23
+ if (attribute[1].toLowerCase() === "srcset") {
24
+ if (!value.startsWith("data:"))
25
+ references.push(...value.split(",").map(item => item.trim().split(/\s+/)[0]));
26
+ }
27
+ else
28
+ references.push(value);
29
+ }
30
+ }
31
+ return references;
32
+ }
33
+ export async function inspectFrontendLinks(directory, { origins = [], workerPatterns = [] } = {}) {
34
+ const files = await frontendFiles(directory);
35
+ const existing = new Set(files);
36
+ const redirectsText = await readFile(resolve(directory, "_redirects"), "utf8").catch(() => "");
37
+ const redirects = new Map(redirectsText.split("\n").filter(line => line.trim() && !line.startsWith("#")).map(line => {
38
+ const [source, target] = line.trim().split(/\s+/);
39
+ return [source, target];
40
+ }));
41
+ const origin = origins[0] || "https://frontend.test";
42
+ const localOrigins = new Set([origin, ...origins]);
43
+ const paths = new Set(["/"]);
44
+ for (const file of files.filter(file => file.startsWith("/_astro/")))
45
+ paths.add(file);
46
+ const broken = [];
47
+ const hasRoute = path => existing.has(path) || existing.has(`${path.replace(/\/$/, "")}/index.html`) || existing.has(`${path}.html`) || invokesWorkerFirst(workerPatterns, path);
48
+ function resolves(path, seen = new Set()) {
49
+ if (seen.has(path))
50
+ return false;
51
+ seen.add(path);
52
+ if (redirects.has(path)) {
53
+ const target = new URL(redirects.get(path), origin);
54
+ return !localOrigins.has(target.origin) || resolves(decodeURIComponent(target.pathname), seen);
55
+ }
56
+ return hasRoute(path);
57
+ }
58
+ for (const file of files.filter(file => file.endsWith(".html") && file !== "/404.html")) {
59
+ const route = file.replace(/index\.html$/, "").replace(/\.html$/, "");
60
+ paths.add(route || "/");
61
+ for (const reference of htmlReferences(await readFile(resolve(directory, file.slice(1)), "utf8"))) {
62
+ if (!reference || reference.startsWith("#"))
63
+ continue;
64
+ const url = new URL(reference, `${origin}${route}`);
65
+ if (!["https:", "http:"].includes(url.protocol) || !localOrigins.has(url.origin))
66
+ continue;
67
+ const path = decodeURIComponent(url.pathname);
68
+ if (!resolves(path))
69
+ broken.push({ source: file, path });
70
+ else if (!invokesWorkerFirst(workerPatterns, path))
71
+ paths.add(path);
72
+ }
73
+ }
74
+ for (const [source] of redirects) {
75
+ if (!resolves(source))
76
+ broken.push({ source: "_redirects", path: source });
77
+ else
78
+ paths.add(source);
79
+ }
80
+ return { paths: [...paths].sort(), broken, pages: files.filter(file => file.endsWith(".html") && file !== "/404.html").length };
81
+ }
82
+ //# sourceMappingURL=frontend-links.js.map