@pitcher/cli-scripts 2.2.0 → 2.2.2
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.
|
@@ -13,6 +13,25 @@ import { createCliError } from '../core/errors.js';
|
|
|
13
13
|
import { emitEnvelope, log } from '../core/output.js';
|
|
14
14
|
const PUBLISH_PATH = '/api/v1/apps/publish/';
|
|
15
15
|
const INSTANCES_PATH = '/api/v1/instances';
|
|
16
|
+
/**
|
|
17
|
+
* The publish endpoint rejects re-publishing an existing version with a 400
|
|
18
|
+
* (`non_field_errors: ["An app release version X already exists."]`). That is a
|
|
19
|
+
* no-op, not a failure — the app is already live at that version — so deploy
|
|
20
|
+
* pipelines that re-run the publish step (workflow re-runs) must not hard-fail
|
|
21
|
+
* on it. Detect it so we can degrade to an idempotent success-with-warning.
|
|
22
|
+
*/
|
|
23
|
+
function isVersionAlreadyExistsError(e) {
|
|
24
|
+
const err = e;
|
|
25
|
+
if (err?.response?.status !== 400)
|
|
26
|
+
return false;
|
|
27
|
+
// Only the version-release conflict — which the API reports in DRF
|
|
28
|
+
// `non_field_errors` — is a benign no-op. Don't scan the whole body: an
|
|
29
|
+
// unrelated 400 that merely mentions "already exists" (e.g. a field-level
|
|
30
|
+
// name/identifier conflict when creating a NEW app) is a real failure.
|
|
31
|
+
const nonField = err.response?.data?.non_field_errors;
|
|
32
|
+
const messages = Array.isArray(nonField) ? nonField : typeof nonField === 'string' ? [nonField] : [];
|
|
33
|
+
return messages.some((m) => typeof m === 'string' && /version .* already exists/i.test(m));
|
|
34
|
+
}
|
|
16
35
|
function buildVersion(now) {
|
|
17
36
|
const year = now.getFullYear();
|
|
18
37
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
@@ -112,11 +131,28 @@ export async function runPublishApp(opts, deps = {}) {
|
|
|
112
131
|
await zipDirectoryToFile(folderPath, zipPath);
|
|
113
132
|
const form = new FormData();
|
|
114
133
|
form.append('file', fs.createReadStream(zipPath));
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
let res;
|
|
135
|
+
try {
|
|
136
|
+
res = await client.post(publishTarget, form, {
|
|
137
|
+
headers: form.getHeaders(),
|
|
138
|
+
maxBodyLength: Infinity,
|
|
139
|
+
maxContentLength: Infinity,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (publishError) {
|
|
143
|
+
// Idempotent re-publish: an existing version is already live, so this is
|
|
144
|
+
// a no-op, not a failure. Return ok with a warning instead of failing the
|
|
145
|
+
// command — otherwise deploy re-runs (which re-publish the same built
|
|
146
|
+
// version) break. Genuine errors still propagate to the outer catch.
|
|
147
|
+
if (isVersionAlreadyExistsError(publishError)) {
|
|
148
|
+
warnings.push(`Skipped publish: ${toCliError(publishError).message}`);
|
|
149
|
+
return ok({
|
|
150
|
+
app: existingAppJson ? { name: existingAppJson.name, version: nextVersion } : undefined,
|
|
151
|
+
skipped: true,
|
|
152
|
+
}, warnings);
|
|
153
|
+
}
|
|
154
|
+
throw publishError;
|
|
155
|
+
}
|
|
120
156
|
const app = res.data?.app;
|
|
121
157
|
const result = { app };
|
|
122
158
|
if (!app?.name) {
|
package/dist/src/core/http.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type AxiosInstance } from 'axios';
|
|
2
2
|
import { type CliError } from './errors.js';
|
|
3
3
|
export declare function makeClient(baseUrl: string, apiKey: string): AxiosInstance;
|
|
4
|
+
/**
|
|
5
|
+
* Build a human-readable message from a server error body. Handles DRF shapes:
|
|
6
|
+
* a `detail`/`error` string, `non_field_errors`, and field-level error arrays
|
|
7
|
+
* (e.g. `{ version: ["already exists"] }`). Falls back to the raw body so the
|
|
8
|
+
* full server response is visible in logs instead of a bare status line.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractApiMessage(data: unknown, fallback: string): string;
|
|
4
11
|
export declare function toCliError(e: unknown): CliError;
|
package/dist/src/core/http.js
CHANGED
|
@@ -9,12 +9,39 @@ export function makeClient(baseUrl, apiKey) {
|
|
|
9
9
|
},
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Build a human-readable message from a server error body. Handles DRF shapes:
|
|
14
|
+
* a `detail`/`error` string, `non_field_errors`, and field-level error arrays
|
|
15
|
+
* (e.g. `{ version: ["already exists"] }`). Falls back to the raw body so the
|
|
16
|
+
* full server response is visible in logs instead of a bare status line.
|
|
17
|
+
*/
|
|
18
|
+
export function extractApiMessage(data, fallback) {
|
|
19
|
+
if (data == null)
|
|
20
|
+
return fallback;
|
|
21
|
+
if (typeof data === 'string')
|
|
22
|
+
return data.trim() || fallback;
|
|
23
|
+
if (typeof data !== 'object')
|
|
24
|
+
return String(data);
|
|
25
|
+
const obj = data;
|
|
26
|
+
if (typeof obj.detail === 'string' && obj.detail)
|
|
27
|
+
return obj.detail;
|
|
28
|
+
if (typeof obj.error === 'string' && obj.error)
|
|
29
|
+
return obj.error;
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
32
|
+
const text = Array.isArray(val) ? val.join('; ') : typeof val === 'string' ? val : JSON.stringify(val);
|
|
33
|
+
parts.push(key === 'non_field_errors' ? text : `${key}: ${text}`);
|
|
34
|
+
}
|
|
35
|
+
if (parts.length)
|
|
36
|
+
return parts.join(' | ');
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
12
39
|
export function toCliError(e) {
|
|
13
40
|
if (isCliError(e))
|
|
14
41
|
return e;
|
|
15
42
|
const err = (e ?? {});
|
|
16
43
|
const data = err.response?.data;
|
|
17
|
-
const apiMessage = data
|
|
44
|
+
const apiMessage = extractApiMessage(data, err.message || 'Request failed');
|
|
18
45
|
const status = err.response?.status;
|
|
19
46
|
if (typeof status === 'number') {
|
|
20
47
|
if (status === 401 || status === 403)
|