@descix/cli 1.0.20 → 1.0.21
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/bin/descix.js +27 -0
- package/lib/deploy-outcome.js +45 -0
- package/package.json +1 -1
package/bin/descix.js
CHANGED
|
@@ -14,6 +14,7 @@ import { requireAuth } from '../lib/auth-guard.js';
|
|
|
14
14
|
import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath } from '../lib/workspace-config.js';
|
|
15
15
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
16
16
|
import { recordInvocationOrigin } from '../lib/origin.js';
|
|
17
|
+
import { verifyDeployOutcome } from '../lib/deploy-outcome.js';
|
|
17
18
|
// Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
|
|
18
19
|
import {
|
|
19
20
|
getSessionInteractionId,
|
|
@@ -2589,6 +2590,28 @@ siteCommand
|
|
|
2589
2590
|
branch: gitStatus.branch
|
|
2590
2591
|
} : null,
|
|
2591
2592
|
preview: options.preview || false
|
|
2593
|
+
}).catch(async (confirmErr) => {
|
|
2594
|
+
// ws-prune-504: a lost confirm RESPONSE is not a failed DEPLOY. Re-read the served
|
|
2595
|
+
// manifest keyed on THIS upload's token and say which it actually is — never "failed"
|
|
2596
|
+
// when the truth is knowable.
|
|
2597
|
+
console.log(chalk.yellow(`\n Confirm response lost (${confirmErr.message}) — reading the deploy record back...`));
|
|
2598
|
+
const verdict = await verifyDeployOutcome(apiClient, {
|
|
2599
|
+
communityId, appId, tokenId: token_id, preview: options.preview || false
|
|
2600
|
+
});
|
|
2601
|
+
if (verdict.outcome === 'confirmed') {
|
|
2602
|
+
console.log(chalk.green(`\n Deploy CONFIRMED — ${verdict.detail}.`));
|
|
2603
|
+
console.log(chalk.gray(` The server may still be finishing the stale-file prune; ` +
|
|
2604
|
+
`'descix site status -c ${communityId} -a ${appId}' shows the deploy, 'descix site list' what is served.`));
|
|
2605
|
+
process.exit(0);
|
|
2606
|
+
}
|
|
2607
|
+
if (verdict.outcome === 'unconfirmed') {
|
|
2608
|
+
console.error(chalk.red(`\n Deploy UNCONFIRMED — ${verdict.detail}.`));
|
|
2609
|
+
console.error(chalk.red(` Nothing was removed from the server. Re-run the upload.`));
|
|
2610
|
+
process.exit(1);
|
|
2611
|
+
}
|
|
2612
|
+
console.error(chalk.red(`\n Deploy outcome UNKNOWN — ${verdict.detail}.`));
|
|
2613
|
+
console.error(chalk.red(` Check 'descix site status -c ${communityId} -a ${appId}' before re-running.`));
|
|
2614
|
+
process.exit(1);
|
|
2592
2615
|
});
|
|
2593
2616
|
|
|
2594
2617
|
const result = confirmResponse.message;
|
|
@@ -2602,6 +2625,10 @@ siteCommand
|
|
|
2602
2625
|
}
|
|
2603
2626
|
console.log(chalk.gray(` Removed from the server: ${pruned.length}`));
|
|
2604
2627
|
pruned.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2628
|
+
if (result.prune_status === 'partial') {
|
|
2629
|
+
console.log(chalk.yellow(` Prune PARTIAL: ${result.prune_remaining_count} stale file(s) not yet removed (soft budget).`));
|
|
2630
|
+
console.log(chalk.yellow(` ${result.prune_remedy}`));
|
|
2631
|
+
}
|
|
2605
2632
|
console.log(chalk.gray(` Deployed: ${result.deployed_at}`));
|
|
2606
2633
|
if (result.preview) {
|
|
2607
2634
|
console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyDeployOutcome — the honest answer when a confirm_site_deploy response is LOST.
|
|
3
|
+
*
|
|
4
|
+
* ws-prune-504 (measured on prod 2026-10-10): a large prune held the confirm past the
|
|
5
|
+
* gateway's patience; the client saw "Upload failed: HTTP 504" while the deploy had in fact
|
|
6
|
+
* confirmed and the prune was finishing server-side. A lost RESPONSE is not a failed DEPLOY —
|
|
7
|
+
* and a tool that says "failed" when it does not know is lying in one direction.
|
|
8
|
+
*
|
|
9
|
+
* The read-back is keyed on the deploy TOKEN: get_site_deploy_token minted token_id for THIS
|
|
10
|
+
* upload, and confirm_site_deploy stamps it into the served manifest's deploy.token_id. If the
|
|
11
|
+
* served manifest carries our token, OUR deploy confirmed — no weaker signal (git sha, file
|
|
12
|
+
* count) can be forged by a concurrent deploy into a false positive the token cannot.
|
|
13
|
+
*
|
|
14
|
+
* @param {{invoke: (cmd: string, params: object) => Promise<any>}} apiClient
|
|
15
|
+
* @param {{communityId: string, appId: string, tokenId: string, preview?: boolean}} sent
|
|
16
|
+
* @returns {Promise<{outcome: 'confirmed'|'unconfirmed'|'unknown', detail: string, deploy?: object}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function verifyDeployOutcome(apiClient, { communityId, appId, tokenId, preview = false }) {
|
|
19
|
+
if (!tokenId) {
|
|
20
|
+
return { outcome: 'unknown', detail: 'no token_id in hand — cannot key the read-back' };
|
|
21
|
+
}
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await apiClient.invoke('get_site_manifest', {
|
|
25
|
+
community_id: communityId,
|
|
26
|
+
app_id: appId,
|
|
27
|
+
preview,
|
|
28
|
+
});
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { outcome: 'unknown', detail: `read-back failed too: ${err.message}` };
|
|
31
|
+
}
|
|
32
|
+
const payload = response?.message ?? response;
|
|
33
|
+
const deploy = payload?.manifest?.deploy;
|
|
34
|
+
if (!payload?.exists || !deploy) {
|
|
35
|
+
return { outcome: 'unconfirmed', detail: 'no deploy manifest is served for this app' };
|
|
36
|
+
}
|
|
37
|
+
if (deploy.token_id === tokenId) {
|
|
38
|
+
return { outcome: 'confirmed', detail: `served manifest carries this upload's token (deployed_at ${deploy.deployed_at})`, deploy };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
outcome: 'unconfirmed',
|
|
42
|
+
detail: `served manifest is a DIFFERENT deploy (token ${deploy.token_id || 'none'}, deployed_at ${deploy.deployed_at || 'unknown'})`,
|
|
43
|
+
deploy,
|
|
44
|
+
};
|
|
45
|
+
}
|