@styleflow.app/flowpress-deploy 1.0.0-beta.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +21 -4
- package/composer.json +1 -1
- package/docs/README.md +16 -0
- package/docs/activities/2026-07-30-2339-initial-pack.md +9 -0
- package/docs/activities/2026-08-24-1745-wordpress-core-v1.md +40 -0
- package/docs/activities/2026-08-26-0926-revision-safe-queue.md +41 -0
- package/docs/activities/README.md +5 -0
- package/docs/architecture.md +19 -0
- package/docs/configuration.md +19 -0
- package/docs/frontend-workflow.md +23 -0
- package/docs/operations.md +15 -0
- package/docs/recovery.md +16 -0
- package/docs/troubleshooting.md +22 -0
- package/docs/wordpress-queue.md +22 -0
- package/flowpress-deploy.php +32 -4
- package/includes/admin.php +110 -0
- package/includes/manual-queue.php +78 -0
- package/includes/queue-provider.php +619 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.1
|
|
4
|
+
|
|
5
|
+
- Added the automatically selected `flowpress-deploy` Core queue provider.
|
|
6
|
+
- Added revision-safe queue, deployed state and batch snapshot tables.
|
|
7
|
+
- Added fingerprint deduplication, WPML post grouping, URI history and safe
|
|
8
|
+
success/failure/stale-batch handling.
|
|
9
|
+
- Added non-destructive migration from the Core database queue.
|
|
10
|
+
- Added end-of-request enqueue, manual editor actions and the expanded
|
|
11
|
+
**Strumenti → FlowPress Deploy** operations screen.
|
|
12
|
+
- Added complete internal documentation and PHP queue contract tests.
|
|
13
|
+
|
|
3
14
|
## 1.0.0-beta.0
|
|
4
15
|
|
|
5
16
|
- Adopted the FlowPress WordPress Core v1 Composer contract.
|
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# FlowPress Deploy
|
|
2
2
|
|
|
3
|
-
Installable npm/Composer feature pack for safe
|
|
4
|
-
build orchestration.
|
|
5
|
-
|
|
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.
|
|
6
7
|
|
|
7
8
|
The public API validates paths and `_routes.json`, merges partial artifacts
|
|
8
9
|
without traversal or case collisions, regenerates sitemaps, selects Pages for
|
|
@@ -10,8 +11,24 @@ static targets and Workers for dev, requires `ASTRO_KEY` for protected batches,
|
|
|
10
11
|
and acknowledges the CMS queue only after deploy and smoke test. Failed
|
|
11
12
|
batches are released for retry.
|
|
12
13
|
|
|
14
|
+
## WordPress queue
|
|
15
|
+
|
|
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
|
+
|
|
13
30
|
## WordPress Core v1
|
|
14
31
|
|
|
15
|
-
The `1.0.0-beta.
|
|
32
|
+
The `1.0.0-beta.x` line requires FlowPress WordPress Core v1. It does not
|
|
16
33
|
provide a v0.x compatibility constraint or adapter; canary applications pin
|
|
17
34
|
the selected Core prerelease exactly.
|
package/composer.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowpress/deploy",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Revision-safe editorial build queue and deploy capability for FlowPress.",
|
|
4
4
|
"type": "wordpress-muplugin",
|
|
5
5
|
"license": "proprietary",
|
|
6
6
|
"require": {
|
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,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.
|
package/docs/recovery.md
ADDED
|
@@ -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.
|
package/flowpress-deploy.php
CHANGED
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
/**
|
|
3
3
|
* Plugin Name: FlowPress Deploy
|
|
4
|
-
* Description:
|
|
5
|
-
* Version:
|
|
4
|
+
* Description: Revision-safe editorial build queue and deployment capability for FlowPress.
|
|
5
|
+
* Version: 1.0.0-beta.1
|
|
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.1');
|
|
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
|
+
|
|
19
|
+
add_action('muplugins_loaded', static function (): void {
|
|
20
|
+
if (! function_exists('flowpress_register_build_queue_provider')) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
flowpress_register_build_queue_provider('flowpress-deploy', [
|
|
24
|
+
'enqueue' => 'flowpress_deploy_collect_change',
|
|
25
|
+
'payload' => 'flowpress_deploy_build_payload',
|
|
26
|
+
'markDispatched' => 'flowpress_deploy_mark_dispatched',
|
|
27
|
+
'complete' => 'flowpress_deploy_complete_batch',
|
|
28
|
+
'pendingCount' => 'flowpress_deploy_pending_count',
|
|
29
|
+
]);
|
|
30
|
+
}, 110);
|
|
31
|
+
|
|
32
|
+
add_filter('flowpress_build_queue_provider', static fn (): string => 'flowpress-deploy', 100);
|
|
33
|
+
|
|
12
34
|
add_action('muplugins_loaded', static function (): void {
|
|
13
35
|
if (! function_exists('flowpress_register_feature')) {
|
|
14
36
|
return;
|
|
15
37
|
}
|
|
16
38
|
flowpress_register_feature('deploy', [
|
|
17
|
-
'version' =>
|
|
18
|
-
'capabilities' => [
|
|
39
|
+
'version' => FLOWPRESS_DEPLOY_VERSION,
|
|
40
|
+
'capabilities' => [
|
|
41
|
+
'full-build',
|
|
42
|
+
'partial-build',
|
|
43
|
+
'build-callback',
|
|
44
|
+
'revision-safe-queue',
|
|
45
|
+
'manual-enqueue',
|
|
46
|
+
],
|
|
19
47
|
'config' => [
|
|
20
48
|
'targets' => function_exists('flowpress_static_targets')
|
|
21
49
|
? 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
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
if (! defined('ABSPATH')) {
|
|
4
|
+
exit;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function flowpress_deploy_manual_url(string $type, int $id): string
|
|
8
|
+
{
|
|
9
|
+
return wp_nonce_url(
|
|
10
|
+
admin_url('admin-post.php?action=flowpress_deploy_enqueue&type=' . rawurlencode($type) . '&id=' . $id),
|
|
11
|
+
'flowpress_deploy_enqueue_' . $type . '_' . $id
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function flowpress_deploy_post_action(array $actions, WP_Post $post): array
|
|
16
|
+
{
|
|
17
|
+
if (current_user_can('edit_post', $post->ID)) {
|
|
18
|
+
$actions['flowpress_deploy_enqueue'] = '<a href="' . esc_url(flowpress_deploy_manual_url('post', $post->ID)) . '">Aggiungi alla build queue</a>';
|
|
19
|
+
}
|
|
20
|
+
return $actions;
|
|
21
|
+
}
|
|
22
|
+
add_filter('post_row_actions', 'flowpress_deploy_post_action', 20, 2);
|
|
23
|
+
add_filter('page_row_actions', 'flowpress_deploy_post_action', 20, 2);
|
|
24
|
+
|
|
25
|
+
add_action('post_submitbox_misc_actions', static function (WP_Post $post): void {
|
|
26
|
+
if (! current_user_can('edit_post', $post->ID)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
echo '<div class="misc-pub-section"><a class="button" href="'
|
|
30
|
+
. esc_url(flowpress_deploy_manual_url('post', $post->ID))
|
|
31
|
+
. '">Aggiungi alla build queue</a></div>';
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
add_filter('term_row_actions', static function (array $actions, WP_Term $term): array {
|
|
35
|
+
$taxonomy = get_taxonomy($term->taxonomy);
|
|
36
|
+
if ($taxonomy && current_user_can($taxonomy->cap->edit_terms)) {
|
|
37
|
+
$actions['flowpress_deploy_enqueue'] = '<a href="'
|
|
38
|
+
. esc_url(flowpress_deploy_manual_url('term-' . $term->taxonomy, $term->term_id))
|
|
39
|
+
. '">Aggiungi alla build queue</a>';
|
|
40
|
+
}
|
|
41
|
+
return $actions;
|
|
42
|
+
}, 20, 2);
|
|
43
|
+
|
|
44
|
+
add_action('admin_post_flowpress_deploy_enqueue', static function (): void {
|
|
45
|
+
$type = sanitize_key((string) ($_GET['type'] ?? ''));
|
|
46
|
+
$id = max(0, (int) ($_GET['id'] ?? 0));
|
|
47
|
+
check_admin_referer('flowpress_deploy_enqueue_' . $type . '_' . $id);
|
|
48
|
+
$uri = '';
|
|
49
|
+
$resource_type = '';
|
|
50
|
+
if ($type === 'post' && current_user_can('edit_post', $id)) {
|
|
51
|
+
$post = get_post($id);
|
|
52
|
+
if ($post instanceof WP_Post) {
|
|
53
|
+
$resource_type = $post->post_type;
|
|
54
|
+
$uri = (string) get_permalink($post);
|
|
55
|
+
}
|
|
56
|
+
} elseif (str_starts_with($type, 'term-')) {
|
|
57
|
+
$taxonomy = substr($type, 5);
|
|
58
|
+
$object = get_taxonomy($taxonomy);
|
|
59
|
+
if ($object && current_user_can($object->cap->edit_terms)) {
|
|
60
|
+
$term = get_term($id, $taxonomy);
|
|
61
|
+
$link = $term instanceof WP_Term ? get_term_link($term) : '';
|
|
62
|
+
if (! is_wp_error($link)) {
|
|
63
|
+
$resource_type = $taxonomy;
|
|
64
|
+
$uri = (string) $link;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if ($resource_type === '' || $uri === '') {
|
|
69
|
+
wp_die('Forbidden', 403);
|
|
70
|
+
}
|
|
71
|
+
flowpress_deploy_enqueue_change($resource_type, $id, $uri, 'update', false);
|
|
72
|
+
set_transient('flowpress_build_notice_' . get_current_user_id(), [
|
|
73
|
+
'type' => 'success',
|
|
74
|
+
'message' => 'Contenuto aggiunto alla build queue.',
|
|
75
|
+
], 60);
|
|
76
|
+
wp_safe_redirect(wp_get_referer() ?: admin_url('tools.php?page=flowpress-builds'));
|
|
77
|
+
exit;
|
|
78
|
+
});
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
if (! defined('ABSPATH')) {
|
|
4
|
+
exit;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function flowpress_deploy_queue_table(): string
|
|
8
|
+
{
|
|
9
|
+
global $wpdb;
|
|
10
|
+
return $wpdb->prefix . 'flowpress_deploy_queue';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function flowpress_deploy_state_table(): string
|
|
14
|
+
{
|
|
15
|
+
global $wpdb;
|
|
16
|
+
return $wpdb->prefix . 'flowpress_deploy_state';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function flowpress_deploy_batch_table(): string
|
|
20
|
+
{
|
|
21
|
+
global $wpdb;
|
|
22
|
+
return $wpdb->prefix . 'flowpress_deploy_batches';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function flowpress_deploy_install_schema(): void
|
|
26
|
+
{
|
|
27
|
+
if (! function_exists('flowpress_wordpress_ready') || ! flowpress_wordpress_ready()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (get_option('flowpress_deploy_schema') === '1') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
global $wpdb;
|
|
35
|
+
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
36
|
+
$charset = $wpdb->get_charset_collate();
|
|
37
|
+
$queue = flowpress_deploy_queue_table();
|
|
38
|
+
$state = flowpress_deploy_state_table();
|
|
39
|
+
$batches = flowpress_deploy_batch_table();
|
|
40
|
+
dbDelta("CREATE TABLE {$queue} (
|
|
41
|
+
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
|
42
|
+
target varchar(64) NOT NULL,
|
|
43
|
+
group_key varchar(191) NOT NULL,
|
|
44
|
+
resource_type varchar(64) NOT NULL,
|
|
45
|
+
resource_id bigint unsigned NOT NULL DEFAULT 0,
|
|
46
|
+
current_uris longtext NOT NULL,
|
|
47
|
+
previous_uris longtext NOT NULL,
|
|
48
|
+
fingerprint char(64) NOT NULL,
|
|
49
|
+
revision bigint unsigned NOT NULL DEFAULT 1,
|
|
50
|
+
force_full tinyint(1) NOT NULL DEFAULT 0,
|
|
51
|
+
batch_id varchar(64) NULL,
|
|
52
|
+
dispatched_revision bigint unsigned NULL,
|
|
53
|
+
dispatched_at datetime NULL,
|
|
54
|
+
created_at datetime NOT NULL,
|
|
55
|
+
updated_at datetime NOT NULL,
|
|
56
|
+
PRIMARY KEY (id),
|
|
57
|
+
UNIQUE KEY target_group (target, group_key),
|
|
58
|
+
KEY batch_id (batch_id),
|
|
59
|
+
KEY target_pending (target, batch_id)
|
|
60
|
+
) {$charset};");
|
|
61
|
+
dbDelta("CREATE TABLE {$state} (
|
|
62
|
+
target varchar(64) NOT NULL,
|
|
63
|
+
group_key varchar(191) NOT NULL,
|
|
64
|
+
current_uris longtext NOT NULL,
|
|
65
|
+
fingerprint char(64) NOT NULL,
|
|
66
|
+
revision bigint unsigned NOT NULL DEFAULT 0,
|
|
67
|
+
updated_at datetime NOT NULL,
|
|
68
|
+
PRIMARY KEY (target, group_key)
|
|
69
|
+
) {$charset};");
|
|
70
|
+
dbDelta("CREATE TABLE {$batches} (
|
|
71
|
+
batch_id varchar(64) NOT NULL,
|
|
72
|
+
target varchar(64) NOT NULL,
|
|
73
|
+
mode varchar(16) NOT NULL,
|
|
74
|
+
snapshot longtext NOT NULL,
|
|
75
|
+
status varchar(16) NOT NULL DEFAULT 'in_flight',
|
|
76
|
+
created_at datetime NOT NULL,
|
|
77
|
+
completed_at datetime NULL,
|
|
78
|
+
PRIMARY KEY (batch_id),
|
|
79
|
+
KEY status_created (status, created_at)
|
|
80
|
+
) {$charset};");
|
|
81
|
+
update_option('flowpress_deploy_schema', '1', false);
|
|
82
|
+
flowpress_deploy_migrate_core_queue();
|
|
83
|
+
}
|
|
84
|
+
add_action('init', 'flowpress_deploy_install_schema', 0);
|
|
85
|
+
|
|
86
|
+
/** @return list<string> */
|
|
87
|
+
function flowpress_deploy_decode_uris(mixed $value): array
|
|
88
|
+
{
|
|
89
|
+
$decoded = is_array($value) ? $value : json_decode((string) $value, true);
|
|
90
|
+
if (! is_array($decoded)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
$uris = [];
|
|
94
|
+
foreach ($decoded as $uri) {
|
|
95
|
+
if (! is_string($uri) || $uri === '') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
$normalized = function_exists('flowpress_normalize_uri')
|
|
99
|
+
? flowpress_normalize_uri($uri)
|
|
100
|
+
: (string) (wp_parse_url($uri, PHP_URL_PATH) ?: '/');
|
|
101
|
+
$uris[$normalized] = true;
|
|
102
|
+
}
|
|
103
|
+
$uris = array_keys($uris);
|
|
104
|
+
sort($uris, SORT_STRING);
|
|
105
|
+
return $uris;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function flowpress_deploy_json(array $value): string
|
|
109
|
+
{
|
|
110
|
+
return (string) wp_json_encode(array_values($value), JSON_UNESCAPED_SLASHES);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @return array{paths:list<string>,deletedPaths:list<string>} */
|
|
114
|
+
function flowpress_deploy_route_delta(array $previous, array $current): array
|
|
115
|
+
{
|
|
116
|
+
$previous = flowpress_deploy_decode_uris($previous);
|
|
117
|
+
$current = flowpress_deploy_decode_uris($current);
|
|
118
|
+
return [
|
|
119
|
+
'paths' => $current,
|
|
120
|
+
'deletedPaths' => array_values(array_diff($previous, $current)),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function flowpress_deploy_ack_action(string $status, int $current_revision, int $snapshot_revision): string
|
|
125
|
+
{
|
|
126
|
+
if ($status !== 'success') {
|
|
127
|
+
return 'release';
|
|
128
|
+
}
|
|
129
|
+
return $current_revision === $snapshot_revision ? 'delete' : 'release';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @return array{resourceType:string,resourceId:int,uri:string,operation:string,forceFull:bool,target:string} */
|
|
133
|
+
function flowpress_deploy_normalize_legacy_row(array $row): array
|
|
134
|
+
{
|
|
135
|
+
return [
|
|
136
|
+
'resourceType' => sanitize_key((string) ($row['resource_type'] ?? 'legacy')) ?: 'legacy',
|
|
137
|
+
'resourceId' => max(0, (int) ($row['resource_id'] ?? 0)),
|
|
138
|
+
'uri' => (string) ($row['uri'] ?? '/'),
|
|
139
|
+
'operation' => ($row['operation'] ?? '') === 'delete' ? 'delete' : 'update',
|
|
140
|
+
'forceFull' => ! empty($row['force_full']),
|
|
141
|
+
'target' => sanitize_key((string) ($row['target'] ?? '')),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @return array{groupKey:string,resourceType:string,resourceId:int,uris:list<string>,fingerprint:string} */
|
|
146
|
+
function flowpress_deploy_describe_resource(string $resource_type, int $resource_id, string $fallback_uri): array
|
|
147
|
+
{
|
|
148
|
+
$resource_type = sanitize_key($resource_type);
|
|
149
|
+
$group_key = $resource_type . ':' . max(0, $resource_id);
|
|
150
|
+
$uris = flowpress_deploy_decode_uris([$fallback_uri]);
|
|
151
|
+
$fingerprint_fields = [
|
|
152
|
+
'resourceType' => $resource_type,
|
|
153
|
+
'resourceId' => $resource_id,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
$post = $resource_id > 0 ? get_post($resource_id) : null;
|
|
157
|
+
if ($post instanceof WP_Post) {
|
|
158
|
+
$element_type = 'post_' . $post->post_type;
|
|
159
|
+
$trid = apply_filters('wpml_element_trid', null, $resource_id, $element_type);
|
|
160
|
+
if (is_numeric($trid) && (int) $trid > 0) {
|
|
161
|
+
$group_key = 'wpml:' . $element_type . ':' . (int) $trid;
|
|
162
|
+
}
|
|
163
|
+
$translations = is_numeric($trid)
|
|
164
|
+
? apply_filters('wpml_get_element_translations', [], (int) $trid, $element_type)
|
|
165
|
+
: [];
|
|
166
|
+
$post_ids = [$resource_id];
|
|
167
|
+
if (is_array($translations)) {
|
|
168
|
+
foreach ($translations as $translation) {
|
|
169
|
+
$translated_id = is_object($translation) ? ($translation->element_id ?? 0) : 0;
|
|
170
|
+
if ((int) $translated_id > 0) {
|
|
171
|
+
$post_ids[] = (int) $translated_id;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
$post_ids = array_values(array_unique($post_ids));
|
|
176
|
+
sort($post_ids, SORT_NUMERIC);
|
|
177
|
+
$uris = [];
|
|
178
|
+
$posts = [];
|
|
179
|
+
foreach ($post_ids as $post_id) {
|
|
180
|
+
$translated = get_post($post_id);
|
|
181
|
+
if (! $translated instanceof WP_Post) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if ($translated->post_status === 'publish') {
|
|
185
|
+
$permalink = get_permalink($translated);
|
|
186
|
+
if (is_string($permalink) && $permalink !== '') {
|
|
187
|
+
$uris[] = $permalink;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
$meta = get_post_meta($post_id);
|
|
191
|
+
$excluded = (array) apply_filters('flowpress_deploy_volatile_meta_keys', [
|
|
192
|
+
'_edit_lock', '_edit_last', '_wp_old_slug', '_wp_page_template',
|
|
193
|
+
], 'post', $post_id);
|
|
194
|
+
foreach ($excluded as $key) {
|
|
195
|
+
unset($meta[(string) $key]);
|
|
196
|
+
}
|
|
197
|
+
ksort($meta);
|
|
198
|
+
$terms = wp_get_object_terms($post_id, get_object_taxonomies($translated->post_type), ['fields' => 'tt_ids']);
|
|
199
|
+
if (! is_wp_error($terms)) {
|
|
200
|
+
$terms = array_map('intval', $terms);
|
|
201
|
+
sort($terms, SORT_NUMERIC);
|
|
202
|
+
}
|
|
203
|
+
$posts[] = [
|
|
204
|
+
'id' => $post_id,
|
|
205
|
+
'type' => $translated->post_type,
|
|
206
|
+
'status' => $translated->post_status,
|
|
207
|
+
'slug' => $translated->post_name,
|
|
208
|
+
'title' => $translated->post_title,
|
|
209
|
+
'content' => $translated->post_content,
|
|
210
|
+
'excerpt' => $translated->post_excerpt,
|
|
211
|
+
'parent' => (int) $translated->post_parent,
|
|
212
|
+
'modified' => $translated->post_modified_gmt,
|
|
213
|
+
'meta' => $meta,
|
|
214
|
+
'terms' => is_wp_error($terms) ? [] : $terms,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
$fingerprint_fields['posts'] = $posts;
|
|
218
|
+
} elseif ($resource_id > 0) {
|
|
219
|
+
$term = get_term($resource_id, $resource_type);
|
|
220
|
+
if ($term instanceof WP_Term) {
|
|
221
|
+
$element_type = 'tax_' . $resource_type;
|
|
222
|
+
$trid = apply_filters('wpml_element_trid', null, $term->term_taxonomy_id, $element_type);
|
|
223
|
+
if (is_numeric($trid) && (int) $trid > 0) {
|
|
224
|
+
$group_key = 'wpml:' . $element_type . ':' . (int) $trid;
|
|
225
|
+
}
|
|
226
|
+
$translations = is_numeric($trid)
|
|
227
|
+
? apply_filters('wpml_get_element_translations', [], (int) $trid, $element_type)
|
|
228
|
+
: [];
|
|
229
|
+
$translated_terms = [$term];
|
|
230
|
+
if (is_array($translations)) {
|
|
231
|
+
foreach ($translations as $translation) {
|
|
232
|
+
$tt_id = is_object($translation) ? (int) ($translation->element_id ?? 0) : 0;
|
|
233
|
+
$translated_term = $tt_id > 0
|
|
234
|
+
? get_term_by('term_taxonomy_id', $tt_id, $resource_type)
|
|
235
|
+
: null;
|
|
236
|
+
if ($translated_term instanceof WP_Term) {
|
|
237
|
+
$translated_terms[] = $translated_term;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
$term_map = [];
|
|
242
|
+
foreach ($translated_terms as $translated_term) {
|
|
243
|
+
$term_map[$translated_term->term_id] = $translated_term;
|
|
244
|
+
}
|
|
245
|
+
ksort($term_map, SORT_NUMERIC);
|
|
246
|
+
$uris = [];
|
|
247
|
+
$terms = [];
|
|
248
|
+
foreach ($term_map as $translated_term) {
|
|
249
|
+
$link = get_term_link($translated_term);
|
|
250
|
+
if (! is_wp_error($link) && is_string($link)) {
|
|
251
|
+
$uris[] = $link;
|
|
252
|
+
}
|
|
253
|
+
$meta = get_term_meta($translated_term->term_id);
|
|
254
|
+
$excluded = (array) apply_filters(
|
|
255
|
+
'flowpress_deploy_volatile_meta_keys',
|
|
256
|
+
[],
|
|
257
|
+
'term',
|
|
258
|
+
$translated_term->term_id
|
|
259
|
+
);
|
|
260
|
+
foreach ($excluded as $key) {
|
|
261
|
+
unset($meta[(string) $key]);
|
|
262
|
+
}
|
|
263
|
+
ksort($meta);
|
|
264
|
+
$terms[] = [
|
|
265
|
+
'id' => $translated_term->term_id,
|
|
266
|
+
'taxonomy' => $resource_type,
|
|
267
|
+
'slug' => $translated_term->slug,
|
|
268
|
+
'name' => $translated_term->name,
|
|
269
|
+
'description' => $translated_term->description,
|
|
270
|
+
'parent' => (int) $translated_term->parent,
|
|
271
|
+
'meta' => $meta,
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
$fingerprint_fields['terms'] = $terms;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
$group_key = (string) apply_filters(
|
|
279
|
+
'flowpress_deploy_group_key',
|
|
280
|
+
$group_key,
|
|
281
|
+
$resource_type,
|
|
282
|
+
$resource_id
|
|
283
|
+
);
|
|
284
|
+
$uris = flowpress_deploy_decode_uris((array) apply_filters(
|
|
285
|
+
'flowpress_deploy_canonical_uris',
|
|
286
|
+
$uris,
|
|
287
|
+
$resource_type,
|
|
288
|
+
$resource_id,
|
|
289
|
+
$group_key
|
|
290
|
+
));
|
|
291
|
+
$fingerprint_fields = (array) apply_filters(
|
|
292
|
+
'flowpress_deploy_fingerprint_fields',
|
|
293
|
+
$fingerprint_fields,
|
|
294
|
+
$resource_type,
|
|
295
|
+
$resource_id,
|
|
296
|
+
$group_key
|
|
297
|
+
);
|
|
298
|
+
return [
|
|
299
|
+
'groupKey' => sanitize_key(str_replace(':', '-', $group_key)),
|
|
300
|
+
'resourceType' => $resource_type,
|
|
301
|
+
'resourceId' => max(0, $resource_id),
|
|
302
|
+
'uris' => $uris,
|
|
303
|
+
'fingerprint' => hash('sha256', (string) wp_json_encode($fingerprint_fields)),
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** @var array<string,array{resourceType:string,resourceId:int,uri:string,operation:string,forceFull:bool}> */
|
|
308
|
+
function &flowpress_deploy_collected_changes(): array
|
|
309
|
+
{
|
|
310
|
+
static $changes = [];
|
|
311
|
+
return $changes;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function flowpress_deploy_collect_change(
|
|
315
|
+
string $resource_type,
|
|
316
|
+
int $resource_id,
|
|
317
|
+
string $uri,
|
|
318
|
+
string $operation = 'update',
|
|
319
|
+
bool $force_full = false
|
|
320
|
+
): void {
|
|
321
|
+
if ($operation === 'delete') {
|
|
322
|
+
flowpress_deploy_enqueue_change($resource_type, $resource_id, $uri, 'delete', $force_full);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
$changes = &flowpress_deploy_collected_changes();
|
|
326
|
+
$changes[$resource_type . ':' . $resource_id] = [
|
|
327
|
+
'resourceType' => $resource_type,
|
|
328
|
+
'resourceId' => $resource_id,
|
|
329
|
+
'uri' => $uri,
|
|
330
|
+
'operation' => $operation,
|
|
331
|
+
'forceFull' => $force_full,
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function flowpress_deploy_flush_changes(): void
|
|
336
|
+
{
|
|
337
|
+
$changes = &flowpress_deploy_collected_changes();
|
|
338
|
+
foreach ($changes as $change) {
|
|
339
|
+
flowpress_deploy_enqueue_change(
|
|
340
|
+
$change['resourceType'],
|
|
341
|
+
$change['resourceId'],
|
|
342
|
+
$change['uri'],
|
|
343
|
+
$change['operation'],
|
|
344
|
+
$change['forceFull']
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
$changes = [];
|
|
348
|
+
}
|
|
349
|
+
add_action('shutdown', 'flowpress_deploy_flush_changes', 0);
|
|
350
|
+
|
|
351
|
+
function flowpress_deploy_enqueue_change(
|
|
352
|
+
string $resource_type,
|
|
353
|
+
int $resource_id,
|
|
354
|
+
string $uri,
|
|
355
|
+
string $operation = 'update',
|
|
356
|
+
bool $force_full = false,
|
|
357
|
+
?string $only_target = null
|
|
358
|
+
): void {
|
|
359
|
+
global $wpdb;
|
|
360
|
+
$descriptor = flowpress_deploy_describe_resource($resource_type, $resource_id, $uri);
|
|
361
|
+
if ($operation === 'delete') {
|
|
362
|
+
$descriptor['uris'] = [];
|
|
363
|
+
$descriptor['fingerprint'] = hash('sha256', 'deleted:' . $descriptor['groupKey']);
|
|
364
|
+
}
|
|
365
|
+
$targets = $only_target !== null ? [$only_target] : flowpress_static_targets();
|
|
366
|
+
foreach ($targets as $target) {
|
|
367
|
+
$target = sanitize_key($target);
|
|
368
|
+
if ($target === '') {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
$existing = $wpdb->get_row($wpdb->prepare(
|
|
372
|
+
'SELECT * FROM ' . flowpress_deploy_queue_table() . ' WHERE target = %s AND group_key = %s',
|
|
373
|
+
$target,
|
|
374
|
+
$descriptor['groupKey']
|
|
375
|
+
), ARRAY_A);
|
|
376
|
+
$state = $wpdb->get_row($wpdb->prepare(
|
|
377
|
+
'SELECT * FROM ' . flowpress_deploy_state_table() . ' WHERE target = %s AND group_key = %s',
|
|
378
|
+
$target,
|
|
379
|
+
$descriptor['groupKey']
|
|
380
|
+
), ARRAY_A);
|
|
381
|
+
if (
|
|
382
|
+
! $force_full
|
|
383
|
+
&& $operation !== 'delete'
|
|
384
|
+
&& ! is_array($existing)
|
|
385
|
+
&& is_array($state)
|
|
386
|
+
&& hash_equals((string) $state['fingerprint'], $descriptor['fingerprint'])
|
|
387
|
+
&& flowpress_deploy_decode_uris($state['current_uris']) === $descriptor['uris']
|
|
388
|
+
) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
$previous = is_array($existing)
|
|
392
|
+
? flowpress_deploy_decode_uris($existing['previous_uris'])
|
|
393
|
+
: (is_array($state) ? flowpress_deploy_decode_uris($state['current_uris']) : []);
|
|
394
|
+
if ($operation === 'delete' && $previous === []) {
|
|
395
|
+
$previous = flowpress_deploy_decode_uris([$uri]);
|
|
396
|
+
}
|
|
397
|
+
$now = current_time('mysql', true);
|
|
398
|
+
if (is_array($existing)) {
|
|
399
|
+
$wpdb->update(flowpress_deploy_queue_table(), [
|
|
400
|
+
'resource_type' => $descriptor['resourceType'],
|
|
401
|
+
'resource_id' => $descriptor['resourceId'],
|
|
402
|
+
'current_uris' => flowpress_deploy_json($descriptor['uris']),
|
|
403
|
+
'fingerprint' => $descriptor['fingerprint'],
|
|
404
|
+
'revision' => (int) $existing['revision'] + 1,
|
|
405
|
+
'force_full' => ($force_full || ! empty($existing['force_full'])) ? 1 : 0,
|
|
406
|
+
'updated_at' => $now,
|
|
407
|
+
], ['id' => (int) $existing['id']]);
|
|
408
|
+
} else {
|
|
409
|
+
$wpdb->insert(flowpress_deploy_queue_table(), [
|
|
410
|
+
'target' => $target,
|
|
411
|
+
'group_key' => $descriptor['groupKey'],
|
|
412
|
+
'resource_type' => $descriptor['resourceType'],
|
|
413
|
+
'resource_id' => $descriptor['resourceId'],
|
|
414
|
+
'current_uris' => flowpress_deploy_json($descriptor['uris']),
|
|
415
|
+
'previous_uris' => flowpress_deploy_json($previous),
|
|
416
|
+
'fingerprint' => $descriptor['fingerprint'],
|
|
417
|
+
'revision' => 1,
|
|
418
|
+
'force_full' => $force_full ? 1 : 0,
|
|
419
|
+
'created_at' => $now,
|
|
420
|
+
'updated_at' => $now,
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @return array<string,list<array<string,mixed>>> */
|
|
427
|
+
function &flowpress_deploy_payload_snapshots(): array
|
|
428
|
+
{
|
|
429
|
+
static $snapshots = [];
|
|
430
|
+
return $snapshots;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function flowpress_deploy_build_payload(string $target, bool $force_full = false): array
|
|
434
|
+
{
|
|
435
|
+
flowpress_deploy_flush_changes();
|
|
436
|
+
flowpress_deploy_recover_stale_batches();
|
|
437
|
+
global $wpdb;
|
|
438
|
+
$rows = (array) $wpdb->get_results($wpdb->prepare(
|
|
439
|
+
'SELECT * FROM ' . flowpress_deploy_queue_table()
|
|
440
|
+
. ' WHERE target = %s AND batch_id IS NULL ORDER BY id ASC',
|
|
441
|
+
$target
|
|
442
|
+
), ARRAY_A);
|
|
443
|
+
$paths = [];
|
|
444
|
+
$deleted = [];
|
|
445
|
+
$full = $force_full;
|
|
446
|
+
foreach ($rows as $row) {
|
|
447
|
+
$current = flowpress_deploy_decode_uris($row['current_uris']);
|
|
448
|
+
$previous = flowpress_deploy_decode_uris($row['previous_uris']);
|
|
449
|
+
$delta = flowpress_deploy_route_delta($previous, $current);
|
|
450
|
+
foreach ($delta['paths'] as $path) {
|
|
451
|
+
$paths[$path] = true;
|
|
452
|
+
unset($deleted[$path]);
|
|
453
|
+
}
|
|
454
|
+
foreach ($delta['deletedPaths'] as $path) {
|
|
455
|
+
if (! isset($paths[$path])) {
|
|
456
|
+
$deleted[$path] = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
$full = $full || ! empty($row['force_full']);
|
|
460
|
+
}
|
|
461
|
+
$batch_id = wp_generate_uuid4();
|
|
462
|
+
$snapshots = &flowpress_deploy_payload_snapshots();
|
|
463
|
+
$snapshots[$batch_id] = $rows;
|
|
464
|
+
return [
|
|
465
|
+
'batchId' => $batch_id,
|
|
466
|
+
'target' => sanitize_key($target),
|
|
467
|
+
'mode' => $full ? 'full' : 'partial',
|
|
468
|
+
'paths' => array_keys($paths),
|
|
469
|
+
'deletedPaths' => array_keys($deleted),
|
|
470
|
+
'queueIds' => array_map('intval', array_column($rows, 'id')),
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function flowpress_deploy_mark_dispatched(array $payload): void
|
|
475
|
+
{
|
|
476
|
+
global $wpdb;
|
|
477
|
+
$batch_id = sanitize_text_field((string) ($payload['batchId'] ?? ''));
|
|
478
|
+
$snapshots = &flowpress_deploy_payload_snapshots();
|
|
479
|
+
$rows = $snapshots[$batch_id] ?? [];
|
|
480
|
+
unset($snapshots[$batch_id]);
|
|
481
|
+
$now = current_time('mysql', true);
|
|
482
|
+
$accepted = [];
|
|
483
|
+
foreach ($rows as $row) {
|
|
484
|
+
$updated = $wpdb->query($wpdb->prepare(
|
|
485
|
+
'UPDATE ' . flowpress_deploy_queue_table()
|
|
486
|
+
. ' SET batch_id = %s, dispatched_revision = %d, dispatched_at = %s'
|
|
487
|
+
. ' WHERE id = %d AND revision = %d AND batch_id IS NULL',
|
|
488
|
+
$batch_id,
|
|
489
|
+
(int) $row['revision'],
|
|
490
|
+
$now,
|
|
491
|
+
(int) $row['id'],
|
|
492
|
+
(int) $row['revision']
|
|
493
|
+
));
|
|
494
|
+
if ($updated) {
|
|
495
|
+
$accepted[] = $row;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
$wpdb->replace(flowpress_deploy_batch_table(), [
|
|
499
|
+
'batch_id' => $batch_id,
|
|
500
|
+
'target' => sanitize_key((string) ($payload['target'] ?? '')),
|
|
501
|
+
'mode' => ($payload['mode'] ?? '') === 'full' ? 'full' : 'partial',
|
|
502
|
+
'snapshot' => (string) wp_json_encode($accepted, JSON_UNESCAPED_SLASHES),
|
|
503
|
+
'status' => 'in_flight',
|
|
504
|
+
'created_at' => $now,
|
|
505
|
+
]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function flowpress_deploy_complete_batch(string $batch_id, string $status): int
|
|
509
|
+
{
|
|
510
|
+
global $wpdb;
|
|
511
|
+
$batch = $wpdb->get_row($wpdb->prepare(
|
|
512
|
+
'SELECT * FROM ' . flowpress_deploy_batch_table() . ' WHERE batch_id = %s',
|
|
513
|
+
$batch_id
|
|
514
|
+
), ARRAY_A);
|
|
515
|
+
if (! is_array($batch)) {
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
$rows = json_decode((string) $batch['snapshot'], true);
|
|
519
|
+
$rows = is_array($rows) ? $rows : [];
|
|
520
|
+
$affected = 0;
|
|
521
|
+
foreach ($rows as $snapshot) {
|
|
522
|
+
$current = $wpdb->get_row($wpdb->prepare(
|
|
523
|
+
'SELECT * FROM ' . flowpress_deploy_queue_table() . ' WHERE id = %d',
|
|
524
|
+
(int) ($snapshot['id'] ?? 0)
|
|
525
|
+
), ARRAY_A);
|
|
526
|
+
if (! is_array($current) || (string) $current['batch_id'] !== $batch_id) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if ($status === 'success') {
|
|
530
|
+
$wpdb->replace(flowpress_deploy_state_table(), [
|
|
531
|
+
'target' => (string) $snapshot['target'],
|
|
532
|
+
'group_key' => (string) $snapshot['group_key'],
|
|
533
|
+
'current_uris' => (string) $snapshot['current_uris'],
|
|
534
|
+
'fingerprint' => (string) $snapshot['fingerprint'],
|
|
535
|
+
'revision' => (int) $snapshot['revision'],
|
|
536
|
+
'updated_at' => current_time('mysql', true),
|
|
537
|
+
]);
|
|
538
|
+
}
|
|
539
|
+
$ack_action = flowpress_deploy_ack_action(
|
|
540
|
+
$status,
|
|
541
|
+
(int) $current['revision'],
|
|
542
|
+
(int) $snapshot['revision']
|
|
543
|
+
);
|
|
544
|
+
if ($ack_action === 'delete') {
|
|
545
|
+
$affected += (int) $wpdb->delete(flowpress_deploy_queue_table(), ['id' => (int) $current['id']]);
|
|
546
|
+
} else {
|
|
547
|
+
$affected += (int) $wpdb->update(flowpress_deploy_queue_table(), [
|
|
548
|
+
'batch_id' => null,
|
|
549
|
+
'dispatched_revision' => null,
|
|
550
|
+
'dispatched_at' => null,
|
|
551
|
+
], ['id' => (int) $current['id']]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
$wpdb->update(flowpress_deploy_batch_table(), [
|
|
555
|
+
'status' => $status,
|
|
556
|
+
'completed_at' => current_time('mysql', true),
|
|
557
|
+
], ['batch_id' => $batch_id]);
|
|
558
|
+
return $affected;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function flowpress_deploy_pending_count(string $target): int
|
|
562
|
+
{
|
|
563
|
+
global $wpdb;
|
|
564
|
+
return (int) $wpdb->get_var($wpdb->prepare(
|
|
565
|
+
'SELECT COUNT(*) FROM ' . flowpress_deploy_queue_table()
|
|
566
|
+
. ' WHERE target = %s AND batch_id IS NULL',
|
|
567
|
+
sanitize_key($target)
|
|
568
|
+
));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function flowpress_deploy_recover_stale_batches(): int
|
|
572
|
+
{
|
|
573
|
+
global $wpdb;
|
|
574
|
+
$hours = max(1, (int) apply_filters('flowpress_deploy_stale_batch_hours', 6));
|
|
575
|
+
$cutoff = gmdate('Y-m-d H:i:s', time() - ($hours * HOUR_IN_SECONDS));
|
|
576
|
+
$batch_ids = (array) $wpdb->get_col($wpdb->prepare(
|
|
577
|
+
'SELECT batch_id FROM ' . flowpress_deploy_batch_table()
|
|
578
|
+
. " WHERE status = 'in_flight' AND created_at < %s",
|
|
579
|
+
$cutoff
|
|
580
|
+
));
|
|
581
|
+
$affected = 0;
|
|
582
|
+
foreach ($batch_ids as $batch_id) {
|
|
583
|
+
$affected += flowpress_deploy_complete_batch((string) $batch_id, 'failure');
|
|
584
|
+
}
|
|
585
|
+
return $affected;
|
|
586
|
+
}
|
|
587
|
+
add_action('flowpress_deploy_recover_stale_batches', 'flowpress_deploy_recover_stale_batches');
|
|
588
|
+
add_action('init', static function (): void {
|
|
589
|
+
if (! wp_next_scheduled('flowpress_deploy_recover_stale_batches')) {
|
|
590
|
+
wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'flowpress_deploy_recover_stale_batches');
|
|
591
|
+
}
|
|
592
|
+
}, 20);
|
|
593
|
+
|
|
594
|
+
function flowpress_deploy_migrate_core_queue(): void
|
|
595
|
+
{
|
|
596
|
+
if (get_option('flowpress_deploy_core_queue_migrated') === '1') {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
global $wpdb;
|
|
600
|
+
$core = $wpdb->prefix . 'flowpress_build_queue';
|
|
601
|
+
$like = method_exists($wpdb, 'esc_like') ? $wpdb->esc_like($core) : $core;
|
|
602
|
+
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like)) !== $core) {
|
|
603
|
+
update_option('flowpress_deploy_core_queue_migrated', '1', false);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
$rows = (array) $wpdb->get_results("SELECT * FROM {$core} ORDER BY id ASC", ARRAY_A);
|
|
607
|
+
foreach ($rows as $row) {
|
|
608
|
+
$legacy = flowpress_deploy_normalize_legacy_row($row);
|
|
609
|
+
flowpress_deploy_enqueue_change(
|
|
610
|
+
$legacy['resourceType'],
|
|
611
|
+
$legacy['resourceId'],
|
|
612
|
+
$legacy['uri'],
|
|
613
|
+
$legacy['operation'],
|
|
614
|
+
$legacy['forceFull'],
|
|
615
|
+
$legacy['target']
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
update_option('flowpress_deploy_core_queue_migrated', '1', false);
|
|
619
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@styleflow.app/flowpress-deploy",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"description": "Safe full and partial deployment primitives for FlowPress.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
"files": ["dist", "flowpress-deploy.php", "composer.json", "README.md", "CHANGELOG.md"],
|
|
21
|
+
"files": ["dist", "includes", "docs", "flowpress-deploy.php", "composer.json", "README.md", "CHANGELOG.md"],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc -b",
|
|
24
24
|
"typecheck": "tsc -b --pretty false",
|
|
25
|
-
"test": "vitest run",
|
|
25
|
+
"test": "vitest run && php tests/php/queue-provider-test.php",
|
|
26
26
|
"release:check": "npm run typecheck && npm test && npm pack --dry-run",
|
|
27
27
|
"release:publish:next": "npm publish --access public --tag next",
|
|
28
28
|
"prepublishOnly": "npm run typecheck && npm test"
|