@styleflow.app/flowpress-deploy 1.0.0-beta.0 → 1.0.0-beta.10

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 (114) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +6 -12
  3. package/composer.json +1 -1
  4. package/containers/cms/Dockerfile +35 -0
  5. package/containers/cms/Dockerfile.dockerignore +20 -0
  6. package/containers/cms/healthz.php +17 -0
  7. package/containers/cms/install-languages.sh +16 -0
  8. package/containers/cms/mpm.conf +10 -0
  9. package/containers/cms/php.ini +16 -0
  10. package/containers/cms/sanitize.php +37 -0
  11. package/containers/cms/smtp.php +13 -0
  12. package/containers/cms/vhost.conf +61 -0
  13. package/containers/cms/wp-cli.yml +1 -0
  14. package/containers/php-runtime/Dockerfile +21 -0
  15. package/containers/web-ssr/Dockerfile +25 -0
  16. package/containers/web-ssr/Dockerfile.dockerignore +10 -0
  17. package/containers/web-static/Caddyfile +40 -0
  18. package/containers/web-static/Dockerfile +15 -0
  19. package/containers/web-static/Dockerfile.dockerignore +7 -0
  20. package/dist/frontend-assets.d.ts +2 -0
  21. package/dist/frontend-assets.d.ts.map +1 -0
  22. package/dist/frontend-assets.js +44 -0
  23. package/dist/frontend-assets.js.map +1 -0
  24. package/dist/frontend-links.d.ts +14 -0
  25. package/dist/frontend-links.d.ts.map +1 -0
  26. package/dist/frontend-links.js +82 -0
  27. package/dist/frontend-links.js.map +1 -0
  28. package/dist/ops/bootstrap.d.ts +21 -0
  29. package/dist/ops/bootstrap.d.ts.map +1 -0
  30. package/dist/ops/bootstrap.js +123 -0
  31. package/dist/ops/bootstrap.js.map +1 -0
  32. package/dist/ops/build.d.ts +30 -0
  33. package/dist/ops/build.d.ts.map +1 -0
  34. package/dist/ops/build.js +99 -0
  35. package/dist/ops/build.js.map +1 -0
  36. package/dist/ops/cli.d.ts +3 -0
  37. package/dist/ops/cli.d.ts.map +1 -0
  38. package/dist/ops/cli.js +140 -0
  39. package/dist/ops/cli.js.map +1 -0
  40. package/dist/ops/config.d.ts +54 -0
  41. package/dist/ops/config.d.ts.map +1 -0
  42. package/dist/ops/config.js +163 -0
  43. package/dist/ops/config.js.map +1 -0
  44. package/dist/ops/coolify.d.ts +2 -0
  45. package/dist/ops/coolify.d.ts.map +1 -0
  46. package/dist/ops/coolify.js +44 -0
  47. package/dist/ops/coolify.js.map +1 -0
  48. package/dist/ops/data.d.ts +27 -0
  49. package/dist/ops/data.d.ts.map +1 -0
  50. package/dist/ops/data.js +148 -0
  51. package/dist/ops/data.js.map +1 -0
  52. package/dist/ops/deploy.d.ts +18 -0
  53. package/dist/ops/deploy.d.ts.map +1 -0
  54. package/dist/ops/deploy.js +100 -0
  55. package/dist/ops/deploy.js.map +1 -0
  56. package/dist/ops/editorial.d.ts +23 -0
  57. package/dist/ops/editorial.d.ts.map +1 -0
  58. package/dist/ops/editorial.js +79 -0
  59. package/dist/ops/editorial.js.map +1 -0
  60. package/dist/ops/environment.d.ts +29 -0
  61. package/dist/ops/environment.d.ts.map +1 -0
  62. package/dist/ops/environment.js +159 -0
  63. package/dist/ops/environment.js.map +1 -0
  64. package/dist/ops/health.d.ts +25 -0
  65. package/dist/ops/health.d.ts.map +1 -0
  66. package/dist/ops/health.js +22 -0
  67. package/dist/ops/health.js.map +1 -0
  68. package/dist/ops/monitor.d.ts +23 -0
  69. package/dist/ops/monitor.d.ts.map +1 -0
  70. package/dist/ops/monitor.js +142 -0
  71. package/dist/ops/monitor.js.map +1 -0
  72. package/dist/ops/process.d.ts +9 -0
  73. package/dist/ops/process.d.ts.map +1 -0
  74. package/dist/ops/process.js +48 -0
  75. package/dist/ops/process.js.map +1 -0
  76. package/dist/ops/provision.d.ts +41 -0
  77. package/dist/ops/provision.d.ts.map +1 -0
  78. package/dist/ops/provision.js +257 -0
  79. package/dist/ops/provision.js.map +1 -0
  80. package/dist/ops/release.d.ts +26 -0
  81. package/dist/ops/release.d.ts.map +1 -0
  82. package/dist/ops/release.js +79 -0
  83. package/dist/ops/release.js.map +1 -0
  84. package/dist/ops/s3.d.ts +8 -0
  85. package/dist/ops/s3.d.ts.map +1 -0
  86. package/dist/ops/s3.js +38 -0
  87. package/dist/ops/s3.js.map +1 -0
  88. package/dist/ops/static-config.d.ts +14 -0
  89. package/dist/ops/static-config.d.ts.map +1 -0
  90. package/dist/ops/static-config.js +96 -0
  91. package/dist/ops/static-config.js.map +1 -0
  92. package/dist/worker-assets.d.ts +5 -0
  93. package/dist/worker-assets.d.ts.map +1 -0
  94. package/dist/worker-assets.js +35 -0
  95. package/dist/worker-assets.js.map +1 -0
  96. package/docs/README.md +16 -0
  97. package/docs/activities/2026-07-30-2339-initial-pack.md +9 -0
  98. package/docs/activities/2026-08-24-1745-wordpress-core-v1.md +40 -0
  99. package/docs/activities/2026-08-26-0926-revision-safe-queue.md +41 -0
  100. package/docs/activities/README.md +5 -0
  101. package/docs/architecture.md +19 -0
  102. package/docs/configuration.md +19 -0
  103. package/docs/frontend-workflow.md +23 -0
  104. package/docs/operations.md +15 -0
  105. package/docs/recovery.md +16 -0
  106. package/docs/troubleshooting.md +22 -0
  107. package/docs/wordpress-queue.md +22 -0
  108. package/flowpress-deploy.php +33 -4
  109. package/includes/admin.php +110 -0
  110. package/includes/automatic-build.php +98 -0
  111. package/includes/manual-queue.php +78 -0
  112. package/includes/queue-provider.php +621 -0
  113. package/package.json +50 -7
  114. package/schema/deployment.schema.json +45 -0
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // Turns the static Astro build's Cloudflare-style `_redirects` and `_headers`
3
+ // into Caddy directives, plus the /media and /app/uploads proxies that the
4
+ // Pages Functions used to provide. Output: <dist>/.container/routes.caddy.
5
+ //
6
+ // APP_ENV=production node FlowPress Deploy apps/web/dist-production
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ function quote(value) {
11
+ if (/["\s{}]/.test(value) && !/^"[^"]*"$/.test(value)) {
12
+ return `"${value.replaceAll('"', '\\"')}"`;
13
+ }
14
+ return value;
15
+ }
16
+ // FlowPress routing only emits exact paths (see validateRedirects), so every
17
+ // line is "source target status". Both slash variants of a source redirect.
18
+ export function redirectsToCaddy(text) {
19
+ const lines = [];
20
+ let index = 0;
21
+ for (const raw of text.split(/\r?\n/)) {
22
+ const line = raw.trim();
23
+ if (!line || line.startsWith("#"))
24
+ continue;
25
+ const [source, target, status = "301", ...rest] = line.split(/\s+/);
26
+ if (rest.length || !source?.startsWith("/") || !target) {
27
+ throw new Error(`Unsupported _redirects line: ${line}`);
28
+ }
29
+ if (/[*:]/.test(source)) {
30
+ throw new Error(`Splats and placeholders are not supported: ${line}`);
31
+ }
32
+ if (!["301", "302", "307", "308"].includes(status)) {
33
+ throw new Error(`Unsupported redirect status: ${line}`);
34
+ }
35
+ const variants = source === "/"
36
+ ? ["/"]
37
+ : [source.replace(/\/+$/, ""), `${source.replace(/\/+$/, "")}/`];
38
+ index += 1;
39
+ lines.push(`@redirect${index} path ${variants.map(quote).join(" ")}`);
40
+ lines.push(`redir @redirect${index} ${quote(target)} ${status}`);
41
+ }
42
+ return lines;
43
+ }
44
+ export function headersToCaddy(text) {
45
+ const lines = [];
46
+ let pattern = null;
47
+ for (const raw of text.split(/\r?\n/)) {
48
+ if (!raw.trim() || raw.trim().startsWith("#"))
49
+ continue;
50
+ if (!/^\s/.test(raw)) {
51
+ pattern = raw.trim();
52
+ if (!pattern.startsWith("/"))
53
+ throw new Error(`Unsupported _headers pattern: ${pattern}`);
54
+ continue;
55
+ }
56
+ if (!pattern)
57
+ throw new Error(`Header without a path pattern: ${raw.trim()}`);
58
+ const separator = raw.indexOf(":");
59
+ if (separator < 0)
60
+ throw new Error(`Unsupported _headers line: ${raw.trim()}`);
61
+ const name = raw.slice(0, separator).trim();
62
+ const value = raw.slice(separator + 1).trim();
63
+ const matcher = pattern === "/*" ? "*" : pattern;
64
+ lines.push(`header ${matcher} ${name} ${quote(value)}`);
65
+ }
66
+ return lines;
67
+ }
68
+ export function proxiesToCaddy({ media, backend }) {
69
+ const lines = [];
70
+ const mediaUrl = new URL(media);
71
+ const mediaBase = mediaUrl.pathname.replace(/\/+$/, "");
72
+ lines.push("@flowpress_media path_regexp flowpress_media ^/media(/.+\\.[a-zA-Z0-9]{1,8})$");
73
+ lines.push("handle @flowpress_media {");
74
+ lines.push(`\trewrite * ${mediaBase}{http.regexp.flowpress_media.1}`);
75
+ lines.push(`\treverse_proxy ${mediaUrl.origin} {`);
76
+ lines.push("\t\theader_up Host {upstream_hostport}");
77
+ lines.push("\t}");
78
+ lines.push("}");
79
+ const backendUrl = new URL(backend);
80
+ lines.push("handle /app/uploads/* {");
81
+ lines.push(`\treverse_proxy ${backendUrl.origin} {`);
82
+ lines.push("\t\theader_up Host {upstream_hostport}");
83
+ lines.push("\t}");
84
+ lines.push("}");
85
+ return lines;
86
+ }
87
+ export function buildRoutesCaddy({ redirects = "", headers = "", media, backend }) {
88
+ return [
89
+ "# Generated by FlowPress Deploy. Do not edit.",
90
+ ...redirectsToCaddy(redirects),
91
+ ...headersToCaddy(headers),
92
+ ...proxiesToCaddy({ media, backend }),
93
+ "",
94
+ ].join("\n");
95
+ }
96
+ //# sourceMappingURL=static-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-config.js","sourceRoot":"","sources":["../../src/ops/static-config.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Match Wrangler's selective Worker-first patterns, including exclusions. */
2
+ export function invokesWorkerFirst(patterns: any, pathname: any): boolean;
3
+ export function validateWorkerAssets(config: any, staticPaths: any, reservedAreaEnabled?: boolean): void;
4
+ export function assertFreeRuntimeRelease(target: any, env: any): void;
5
+ //# sourceMappingURL=worker-assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-assets.d.ts","sourceRoot":"","sources":["../src/worker-assets.js"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,0EAMC;AAED,yGAYC;AAED,sEAIC"}
@@ -0,0 +1,35 @@
1
+ /** Match Wrangler's selective Worker-first patterns, including exclusions. */
2
+ export function invokesWorkerFirst(patterns, pathname) {
3
+ const match = pattern => new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*")}$`).test(pathname);
4
+ if (patterns === true)
5
+ return true;
6
+ if (!Array.isArray(patterns))
7
+ return false;
8
+ return patterns.some(pattern => !pattern.startsWith("!") && match(pattern))
9
+ && !patterns.some(pattern => pattern.startsWith("!") && match(pattern.slice(1)));
10
+ }
11
+ export function validateWorkerAssets(config, staticPaths, reservedAreaEnabled = false) {
12
+ const patterns = config.assets?.run_worker_first;
13
+ if (config.assets?.binding !== "ASSETS" || !config.main)
14
+ throw new Error("Worker must have an ASSETS binding and compiled entrypoint");
15
+ if (reservedAreaEnabled && (patterns === true || patterns?.includes("/*")))
16
+ throw new Error("Public assets must bypass the Worker");
17
+ for (const path of (reservedAreaEnabled ? ["/area-riservata/", "/area-riservata/example/", "/api/flowpress/reserved-area/login", "/api/flowpress/reserved-area/logout"] : [])) {
18
+ if (!invokesWorkerFirst(patterns, path))
19
+ throw new Error(`Protected runtime route missing: ${path}`);
20
+ }
21
+ for (const path of staticPaths) {
22
+ if (path.startsWith("/area-riservata/") || path.startsWith("/api/flowpress/reserved-area/"))
23
+ throw new Error(`Private static asset forbidden: ${path}`);
24
+ if (reservedAreaEnabled && invokesWorkerFirst(patterns, path))
25
+ throw new Error(`Static asset invokes Worker: ${path}`);
26
+ }
27
+ if (reservedAreaEnabled && (config.kv_namespaces?.length || config.images))
28
+ throw new Error("Free runtime must not provision session KV or Images bindings");
29
+ }
30
+ export function assertFreeRuntimeRelease(target, env) {
31
+ if (target.environmentName === "production" && target.projectType === "workers" && env.FLOWPRESS_RESERVED_AREA_ENABLED === "true" && env.CF_FREE_RUNTIME_VERIFIED !== "true") {
32
+ throw new Error("Production Workers release is gated: validate staging CPU (p95 < 8 ms, p99 < 10 ms, no exceededCpu), then set CF_FREE_RUNTIME_VERIFIED=true in the production environment.");
33
+ }
34
+ }
35
+ //# sourceMappingURL=worker-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-assets.js","sourceRoot":"","sources":["../src/worker-assets.js"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,UAAU,kBAAkB,CAAC,QAAQ,EAAE,QAAQ;IACnD,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/H,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;WACtE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,mBAAmB,GAAG,KAAK;IACnF,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC;IACjD,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IACvI,IAAI,mBAAmB,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACpI,KAAK,MAAM,IAAI,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,kBAAkB,EAAE,0BAA0B,EAAE,oCAAoC,EAAE,qCAAqC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAC9K,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,EAAE,CAAC,CAAC;IACvG,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,+BAA+B,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;QACxJ,IAAI,mBAAmB,IAAI,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;IACzH,CAAC;IACD,IAAI,mBAAmB,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;AAC/J,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,MAAM,EAAE,GAAG;IAClD,IAAI,MAAM,CAAC,eAAe,KAAK,YAAY,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,GAAG,CAAC,+BAA+B,KAAK,MAAM,IAAI,GAAG,CAAC,wBAAwB,KAAK,MAAM,EAAE,CAAC;QAC7K,MAAM,IAAI,KAAK,CAAC,4KAA4K,CAAC,CAAC;IAChM,CAAC;AACH,CAAC"}
package/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # FlowPress Deploy: documentazione
2
+
3
+ FlowPress Deploy collega le modifiche editoriali di WordPress ai workflow di
4
+ build senza salvare token o contenuti nella propria coda.
5
+
6
+ ## Pagine
7
+
8
+ | Argomento | Documento |
9
+ | --- | --- |
10
+ | Modello e responsabilità | [Architettura](architecture.md) |
11
+ | Variabili richieste | [Configurazione](configuration.md) |
12
+ | Deduplica, URI e revisioni | [Queue WordPress](wordpress-queue.md) |
13
+ | Payload e callback | [Frontend e workflow](frontend-workflow.md) |
14
+ | Uso quotidiano | [Operazioni](operations.md) |
15
+ | Callback mancanti e retry | [Recovery](recovery.md) |
16
+ | Diagnostica | [Troubleshooting](troubleshooting.md) |
@@ -0,0 +1,9 @@
1
+ # Release iniziale deploy pack
2
+
3
+ - Obiettivo: estrarre build full/partial, baseline e guardie Cloudflare.
4
+ - Modifiche: package npm, MU-plugin Composer, test e workflow OIDC.
5
+ - Verifiche: typecheck, 7 test, npm pack, Composer validate e PHP lint positivi.
6
+ - Hardening pre-release: merge sicuro full/partial, sitemap, target
7
+ Pages/Workers, guardia `ASTRO_KEY` e lifecycle ack/release della queue.
8
+ - Workflow tag idempotente per il bootstrap della prima pubblicazione npm.
9
+ - Release prevista: `0.1.0-beta.0`; resta da configurare il Trusted Publisher npm.
@@ -0,0 +1,40 @@
1
+ # Deploy su WordPress Core v1
2
+
3
+ ## Esito
4
+
5
+ Preparata la major prerelease `1.0.0-beta.0` npm/Composer con dipendenza
6
+ limitata a `flowpress/wordpress-core ^1.0.0@beta` e senza adapter v0.x. Il
7
+ consumer canary mantiene il pin Core esatto.
8
+
9
+ ## Scope
10
+
11
+ La logica Deploy non cambia. Versione package, Composer contract, lockfile,
12
+ changelog e documentazione vengono pubblicati dallo stesso tag.
13
+
14
+ ## Verifiche
15
+
16
+ | Gate | Esito |
17
+ |---|---|
18
+ | `npm ci && npm run release:check` | OK; 7/7 test e pack 9 file |
19
+ | `npm audit` | OK; 0 vulnerabilita' dopo aggiornamento lockfile `nanoid` |
20
+ | `composer validate --strict --no-check-publish` | OK |
21
+ | PHP lint | OK |
22
+ | `git diff --check` | OK |
23
+
24
+ ## Pubblicazione verificata
25
+
26
+ - PR v1 unita su `main`; merge e `gitHead` npm:
27
+ `24e0033d78566890263249e56da3c15449e80897`.
28
+ - Workflow Publish `32747502576`: verde con trusted publishing e provenance.
29
+ - npm: `@styleflow.app/flowpress-deploy@1.0.0-beta.0`, dist-tag `next`;
30
+ integrity, shasum, tarball e firma presenti. `latest` resta sulla linea
31
+ precedente.
32
+ - Repman: `flowpress/deploy@v1.0.0-beta.0`, sincronizzato dallo stesso tag e
33
+ con requisito `flowpress/wordpress-core ^1.0.0@beta`.
34
+ - Canary DMO: installazione npm/Composer da registry, manifest coerente e gate
35
+ completo verde.
36
+
37
+ ## Rollback
38
+
39
+ La linea `0.1.0-beta.0` resta immutabile. I consumer v1 usano pin esatti e non
40
+ spostano `latest`.
@@ -0,0 +1,41 @@
1
+ # FlowPress Deploy: revision-safe editorial queue
2
+
3
+ - Data: 2026-08-26 09:26 Europe/Rome
4
+ - Repository: `StyleFlow-App/flowpress-deploy`
5
+ - Versione prevista: `1.0.0-beta.1`
6
+
7
+ ## Obiettivo
8
+
9
+ Rendere Deploy proprietario della queue editoriale tramite il provider Core,
10
+ senza cambiare Core e senza assumere configurazioni Bagful/Cloudflare.
11
+
12
+ ## Modifiche
13
+
14
+ - Provider automatico, schema queue/state/batch e migrazione Core non
15
+ distruttiva.
16
+ - Fingerprint, URI history, WPML grouping, snapshot e ack revision-safe.
17
+ - Enqueue a fine richiesta, azioni manuali, UI operativa e GitHub test.
18
+ - Documentazione completa in `docs/` e inclusione nel pacchetto npm.
19
+
20
+ ## Verifiche
21
+
22
+ - `php tests/php/queue-provider-test.php`: superato.
23
+ - `find . -name '*.php' ... php -l`: superato.
24
+ - `git diff --check`: superato.
25
+ - `composer validate --strict`: superato.
26
+ - `npm run release:check`: superato (typecheck, 7 test Vitest, test PHP,
27
+ pack dry-run con `docs/` e `includes/`).
28
+
29
+ ## Verifica manuale
30
+
31
+ La verifica integrata in WordPress DMO sarà registrata nel report DMO; nessun
32
+ dispatch o deploy remoto è autorizzato in questo task.
33
+
34
+ ## Commit suggerito
35
+
36
+ `feat: add revision-safe editorial build queue`
37
+
38
+ ## Rischi e follow-up
39
+
40
+ Il rollout richiede configurazione remota dei token e un test editoriale
41
+ controllato prima di abilitare la pubblicazione production.
@@ -0,0 +1,5 @@
1
+ # Indice attività
2
+
3
+ - [2026-08-26-0926-revision-safe-queue.md](2026-08-26-0926-revision-safe-queue.md)
4
+ - [2026-08-24-1745-wordpress-core-v1.md](2026-08-24-1745-wordpress-core-v1.md)
5
+ - [2026-07-30-2339-initial-pack.md](2026-07-30-2339-initial-pack.md)
@@ -0,0 +1,19 @@
1
+ # Architettura
2
+
3
+ ## Perché
4
+
5
+ Core espone il contratto di dispatch e callback; Deploy possiede lo stato
6
+ editoriale necessario a non perdere aggiornamenti concorrenti.
7
+
8
+ ## Come funziona
9
+
10
+ 1. Il plugin registra il provider `flowpress-deploy` e lo seleziona.
11
+ 2. Gli hook Core inoltrano post, termini e modifiche globali al provider.
12
+ 3. Gli update vengono materializzati a `shutdown`, dopo meta e tassonomie.
13
+ 4. Le tabelle `flowpress_deploy_queue`, `flowpress_deploy_state` e
14
+ `flowpress_deploy_batches` conservano rispettivamente lavoro, ultimo stato
15
+ distribuito e snapshot immutabile del batch.
16
+ 5. Core effettua repository dispatch e callback usando il wire contract
17
+ pubblico. Deploy applica l’ack alla sola revisione fotografata.
18
+
19
+ La coda conserva hash SHA-256 e URI, non il contenuto completo o credenziali.
@@ -0,0 +1,19 @@
1
+ # Configurazione
2
+
3
+ ## Perché
4
+
5
+ Target e credenziali sono configurazione di ambiente e non devono finire nel
6
+ database WordPress.
7
+
8
+ ## Come fare
9
+
10
+ 1. Definire `SF_STATIC_TARGETS_JSON`, per esempio `['production']` in JSON
11
+ valido.
12
+ 2. Definire `GITHUB_REPOSITORY_SLUG=organizzazione/repository`.
13
+ 3. Definire `CMS_GITHUB_DISPATCH_TOKEN` con il minimo accesso necessario al
14
+ repository.
15
+ 4. Definire `CMS_BUILD_CALLBACK_TOKEN` sia nel backend sia nel workflow.
16
+ 5. Aprire **Strumenti → FlowPress Deploy**: lo stato indica soltanto presenza
17
+ o assenza dei token, mai il loro valore.
18
+
19
+ ⚠️ Un target deve corrispondere esattamente a quello gestito dal workflow.
@@ -0,0 +1,23 @@
1
+ # Frontend e workflow
2
+
3
+ ## Wire contract
4
+
5
+ Il repository dispatch riceve:
6
+
7
+ ```json
8
+ {
9
+ "batchId": "uuid",
10
+ "target": "production",
11
+ "mode": "partial",
12
+ "paths": ["/pagina/"],
13
+ "deletedPaths": ["/vecchio-slug/"],
14
+ "queueIds": [12]
15
+ }
16
+ ```
17
+
18
+ Il workflow deve chiamare `POST /flowpress/v1/build-complete` con bearer token
19
+ e body `{"batchId":"uuid","status":"success"}` soltanto dopo build, deploy
20
+ e smoke test. In ogni altro caso invia `failure`.
21
+
22
+ ⚠️ L’assenza del callback non equivale a successo: il batch resta in-flight e
23
+ sarà rilasciato dal recovery.
@@ -0,0 +1,15 @@
1
+ # Operazioni
2
+
3
+ ## Come fare
4
+
5
+ 1. Aprire **Strumenti → FlowPress Deploy**.
6
+ 2. Verificare repository, token, righe pending, URI e batch in-flight.
7
+ 3. Espandere **Payload preview** prima dell’invio.
8
+ 4. Usare **Pubblica queue** per la modalità prevista.
9
+ 5. Usare **Force full** soltanto quando baseline o codice richiedono una build
10
+ completa; il browser chiede conferma.
11
+ 6. Per forzare un singolo contenuto usare **Aggiungi alla build queue** nella
12
+ schermata o nella lista di post/termini.
13
+
14
+ Il pulsante **Test GitHub** verifica autenticazione e accesso al repository,
15
+ senza inviare un deployment.
@@ -0,0 +1,16 @@
1
+ # Recovery
2
+
3
+ ## Callback failure
4
+
5
+ 1. Il callback `failure` libera il batch.
6
+ 2. Le righe tornano pending mantenendo l’ultima revisione editoriale.
7
+ 3. Correggere la causa nel workflow e ripubblicare la queue.
8
+
9
+ ## Callback mancante
10
+
11
+ 1. Il cron orario cerca batch in-flight più vecchi di sei ore.
12
+ 2. Il batch viene rilasciato come failure.
13
+ 3. Il limite è modificabile con `flowpress_deploy_stale_batch_hours`.
14
+
15
+ ⚠️ Non cancellare manualmente righe delle tre tabelle: si perderebbe la
16
+ corrispondenza fra stato distribuito e snapshot. Usare callback/retry.
@@ -0,0 +1,22 @@
1
+ # Troubleshooting
2
+
3
+ ## Queue vuota
4
+
5
+ Controllare `SF_STATIC_TARGETS_JSON`, tipo di contenuto instradabile e che il
6
+ salvataggio non sia autosave/revisione. Un salvataggio identico è correttamente
7
+ deduplicato.
8
+
9
+ ## URI vecchio non eliminato
10
+
11
+ Controllare `previous_uris` nella pagina Deploy e che il workflow consumi
12
+ `deletedPaths`.
13
+
14
+ ## Batch sempre in-flight
15
+
16
+ Verificare token callback e log del workflow. Attendere il recovery oppure
17
+ inviare esplicitamente `failure` per il `batchId` corretto.
18
+
19
+ ## GitHub rifiuta il test
20
+
21
+ Verificare slug repository, scadenza e permessi del token. La pagina non mostra
22
+ né registra il segreto.
@@ -0,0 +1,22 @@
1
+ # Queue WordPress
2
+
3
+ ## Perché
4
+
5
+ Salvataggi ripetuti, traduzioni e cambi slug non devono produrre build inutili
6
+ o lasciare file vecchi online.
7
+
8
+ ## Come funziona
9
+
10
+ 1. Autosave e revisioni sono esclusi da Core.
11
+ 2. Deploy raggruppa per post/term ID; con WPML usa il translation group.
12
+ 3. Il fingerprint include contenuto, meta non volatili e tassonomie. Il filtro
13
+ `flowpress_deploy_volatile_meta_keys` aggiunge esclusioni.
14
+ 4. `flowpress_deploy_fingerprint_fields`, `flowpress_deploy_group_key` e
15
+ `flowpress_deploy_canonical_uris` permettono estensioni senza patch.
16
+ 5. `previous_uris` resta ancorato allo stato distribuito: uno slug change
17
+ produce il nuovo path e inserisce quello vecchio in `deletedPaths`.
18
+ 6. Una modifica durante un batch incrementa la revisione. Il callback completa
19
+ lo snapshot, ma rimette la revisione nuova in pending.
20
+
21
+ La prima installazione importa le righe Core senza cancellarle; ogni vecchio
22
+ batch in-flight viene importato come pending per un retry sicuro.
@@ -1,21 +1,50 @@
1
1
  <?php
2
2
  /**
3
3
  * Plugin Name: FlowPress Deploy
4
- * Description: Installable deployment capability for FlowPress.
5
- * Version: 0.1.0-beta.0
4
+ * Description: Revision-safe editorial build queue and deployment capability for FlowPress.
5
+ * Version: 1.0.0-beta.5
6
6
  */
7
7
 
8
8
  if (! defined('ABSPATH')) {
9
9
  exit;
10
10
  }
11
11
 
12
+ define('FLOWPRESS_DEPLOY_VERSION', '1.0.0-beta.5');
13
+ define('FLOWPRESS_DEPLOY_FILE', __FILE__);
14
+
15
+ require_once __DIR__ . '/includes/queue-provider.php';
16
+ require_once __DIR__ . '/includes/manual-queue.php';
17
+ require_once __DIR__ . '/includes/admin.php';
18
+ require_once __DIR__ . '/includes/automatic-build.php';
19
+
20
+ add_action('muplugins_loaded', static function (): void {
21
+ if (! function_exists('flowpress_register_build_queue_provider')) {
22
+ return;
23
+ }
24
+ flowpress_register_build_queue_provider('flowpress-deploy', [
25
+ 'enqueue' => 'flowpress_deploy_collect_change',
26
+ 'payload' => 'flowpress_deploy_build_payload',
27
+ 'markDispatched' => 'flowpress_deploy_mark_dispatched',
28
+ 'complete' => 'flowpress_deploy_complete_batch',
29
+ 'pendingCount' => 'flowpress_deploy_pending_count',
30
+ ]);
31
+ }, 110);
32
+
33
+ add_filter('flowpress_build_queue_provider', static fn (): string => 'flowpress-deploy', 100);
34
+
12
35
  add_action('muplugins_loaded', static function (): void {
13
36
  if (! function_exists('flowpress_register_feature')) {
14
37
  return;
15
38
  }
16
39
  flowpress_register_feature('deploy', [
17
- 'version' => '0.1.0-beta.0',
18
- 'capabilities' => ['full-build', 'partial-build', 'build-callback'],
40
+ 'version' => FLOWPRESS_DEPLOY_VERSION,
41
+ 'capabilities' => [
42
+ 'full-build',
43
+ 'partial-build',
44
+ 'build-callback',
45
+ 'revision-safe-queue',
46
+ 'manual-enqueue',
47
+ ],
19
48
  'config' => [
20
49
  'targets' => function_exists('flowpress_static_targets')
21
50
  ? flowpress_static_targets()
@@ -0,0 +1,110 @@
1
+ <?php
2
+
3
+ if (! defined('ABSPATH')) {
4
+ exit;
5
+ }
6
+
7
+ add_action('admin_menu', static function (): void {
8
+ remove_submenu_page('tools.php', 'flowpress-builds');
9
+ add_management_page(
10
+ 'FlowPress Deploy',
11
+ 'FlowPress Deploy',
12
+ 'manage_options',
13
+ 'flowpress-builds',
14
+ 'flowpress_deploy_render_admin_page'
15
+ );
16
+ }, 100);
17
+
18
+ add_action('admin_post_flowpress_deploy_test_github', static function (): void {
19
+ if (! current_user_can('manage_options')) {
20
+ wp_die('Forbidden', 403);
21
+ }
22
+ check_admin_referer('flowpress_deploy_test_github');
23
+ $repository = flowpress_env('GITHUB_REPOSITORY_SLUG');
24
+ $token = flowpress_env('CMS_GITHUB_DISPATCH_TOKEN');
25
+ $response = ($repository !== '' && $token !== '')
26
+ ? wp_remote_get('https://api.github.com/repos/' . $repository, [
27
+ 'timeout' => 15,
28
+ 'headers' => [
29
+ 'Accept' => 'application/vnd.github+json',
30
+ 'Authorization' => 'Bearer ' . $token,
31
+ 'User-Agent' => 'FlowPress-Deploy/' . FLOWPRESS_DEPLOY_VERSION,
32
+ ],
33
+ ])
34
+ : new WP_Error('missing_config', 'Configurazione GitHub incompleta.');
35
+ $ok = ! is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200;
36
+ set_transient('flowpress_build_notice_' . get_current_user_id(), [
37
+ 'type' => $ok ? 'success' : 'error',
38
+ 'message' => $ok ? 'Connessione GitHub verificata.' : (is_wp_error($response) ? $response->get_error_message() : 'GitHub ha rifiutato la richiesta.'),
39
+ ], 60);
40
+ wp_safe_redirect(admin_url('tools.php?page=flowpress-builds'));
41
+ exit;
42
+ });
43
+
44
+ function flowpress_deploy_render_admin_page(): void
45
+ {
46
+ if (! current_user_can('manage_options')) {
47
+ return;
48
+ }
49
+ global $wpdb;
50
+ $notice = get_transient('flowpress_build_notice_' . get_current_user_id());
51
+ delete_transient('flowpress_build_notice_' . get_current_user_id());
52
+ $repository = flowpress_env('GITHUB_REPOSITORY_SLUG');
53
+ $has_dispatch_token = flowpress_env('CMS_GITHUB_DISPATCH_TOKEN') !== '';
54
+ $has_callback_token = flowpress_env('CMS_BUILD_CALLBACK_TOKEN') !== '';
55
+ echo '<div class="wrap"><h1>FlowPress Deploy</h1>';
56
+ if (is_array($notice)) {
57
+ printf('<div class="notice notice-%s"><p>%s</p></div>', esc_attr((string) $notice['type']), esc_html((string) $notice['message']));
58
+ }
59
+ echo '<p>La coda è deduplicata per contenuto logico e target. I token non vengono salvati né mostrati.</p>';
60
+ echo '<table class="widefat striped" style="max-width:900px"><tbody>';
61
+ printf('<tr><th>Repository GitHub</th><td>%s</td></tr>', esc_html($repository !== '' ? $repository : 'non configurato'));
62
+ printf('<tr><th>Token dispatch</th><td>%s</td></tr>', $has_dispatch_token ? 'configurato' : 'mancante');
63
+ printf('<tr><th>Token callback</th><td>%s</td></tr>', $has_callback_token ? 'configurato' : 'mancante');
64
+ echo '</tbody></table>';
65
+ echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:16px 0">';
66
+ wp_nonce_field('flowpress_deploy_test_github');
67
+ echo '<input type="hidden" name="action" value="flowpress_deploy_test_github">';
68
+ submit_button('Test GitHub', 'secondary', 'submit', false);
69
+ echo '</form>';
70
+ foreach (flowpress_static_targets() as $target) {
71
+ $payload = flowpress_deploy_build_payload($target, false);
72
+ $rows = (array) $wpdb->get_results($wpdb->prepare(
73
+ 'SELECT * FROM ' . flowpress_deploy_queue_table() . ' WHERE target = %s ORDER BY updated_at DESC LIMIT 100',
74
+ $target
75
+ ), ARRAY_A);
76
+ $in_flight = (int) $wpdb->get_var($wpdb->prepare(
77
+ "SELECT COUNT(*) FROM " . flowpress_deploy_batch_table() . " WHERE target = %s AND status = 'in_flight'",
78
+ $target
79
+ ));
80
+ echo '<section style="margin:20px 0;padding:16px;background:#fff;border:1px solid #ccd0d4">';
81
+ printf('<h2>%s <small>(%d pending, %d in-flight)</small></h2>', esc_html($target), flowpress_deploy_pending_count($target), $in_flight);
82
+ echo '<p>Modalità prevista: <strong>' . esc_html($payload['mode']) . '</strong></p>';
83
+ echo '<details><summary>Payload preview</summary><pre style="white-space:pre-wrap">' . esc_html((string) wp_json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '</pre></details>';
84
+ echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin:12px 8px 12px 0">';
85
+ wp_nonce_field('flowpress_publish_build');
86
+ echo '<input type="hidden" name="action" value="flowpress_publish_build"><input type="hidden" name="target" value="' . esc_attr($target) . '">';
87
+ submit_button('Pubblica queue', 'primary', 'submit', false);
88
+ echo '</form>';
89
+ echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block" onsubmit="return confirm(\'Confermi la build completa?\')">';
90
+ wp_nonce_field('flowpress_publish_build');
91
+ echo '<input type="hidden" name="action" value="flowpress_publish_build"><input type="hidden" name="target" value="' . esc_attr($target) . '"><input type="hidden" name="force_full" value="1">';
92
+ submit_button('Force full', 'secondary', 'submit', false);
93
+ echo '</form>';
94
+ if ($rows !== []) {
95
+ echo '<table class="widefat striped"><thead><tr><th>Gruppo</th><th>URI correnti</th><th>URI precedenti</th><th>Rev.</th><th>Stato</th></tr></thead><tbody>';
96
+ foreach ($rows as $row) {
97
+ printf('<tr><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%s</td></tr>',
98
+ esc_html((string) $row['group_key']),
99
+ esc_html(implode(', ', flowpress_deploy_decode_uris($row['current_uris'])) ?: '—'),
100
+ esc_html(implode(', ', flowpress_deploy_decode_uris($row['previous_uris'])) ?: '—'),
101
+ (int) $row['revision'],
102
+ $row['batch_id'] ? 'in-flight' : 'pending'
103
+ );
104
+ }
105
+ echo '</tbody></table>';
106
+ }
107
+ echo '</section>';
108
+ }
109
+ echo '</div>';
110
+ }