pandorga 1.4.4 → 1.4.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -3
- data/functions/api/studio/_lib/github.js +138 -21
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-studio-api-contract.rb +8 -2
- data/scripts/test/test-studio-pipeline-config.mjs +35 -0
- data/scripts/validate.sh +5 -0
- data/studio/assets/{index-Cf9OPjQU.js → index-CMVzh8F1.js} +60 -60
- data/studio/index.html +1 -1
- data/studio-app/src/api/client.js +6 -4
- data/studio-app/src/main.js +71 -61
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8ea9638b4cbf0679fd3eeb328969878e5c4e7080be6a338257ccfc831f66213
|
|
4
|
+
data.tar.gz: 865776c26a7094cdf151ff21c6ecf492dd9e91171a6608bb23567323ff6ef8a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f14807c5d0798c3dd75253faa2571fd142ee5f9e69017486a89cf65eead3c45707b683ab044bd45c3be03fe177287732a0cb95dfc1a57597917ea9a6e2b272b4
|
|
7
|
+
data.tar.gz: 86216efa57c77d99e3f1369d467edd57580f3046e558325b425ad1ecc85c5b0c9dacb162407840229c50f87de9233f8e668ef3abaa5cd2b34714b76644697042
|
data/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.6] — 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Pipeline config resolution no longer swallows `_config.yml` read failures as
|
|
8
|
+
a silent “unconfigured”; returns a clear message when
|
|
9
|
+
`pandorga.content.workflow` is missing
|
|
10
|
+
- Studio chip surfaces pipeline-status API errors on boot, not only after Save
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Gate `test-studio-pipeline-config.mjs` for `parseContentWorkflowFromConfig`
|
|
15
|
+
|
|
16
|
+
## [1.4.5] — 2026-10-07
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Studio pipeline chip is always visible (between Theme and Revert); toolbar
|
|
21
|
+
Delete control removed
|
|
22
|
+
- Pipeline workflow name comes from `_config.yml` → `pandorga.content.workflow`
|
|
23
|
+
(not a Pages secret); env override remains optional
|
|
24
|
+
- Pipeline status without `sha` returns the latest run on the branch
|
|
25
|
+
|
|
3
26
|
## [1.4.4] — 2026-10-07
|
|
4
27
|
|
|
5
28
|
### Added
|
|
6
29
|
|
|
7
30
|
- Studio content-pipeline status: after Save in production, poll GitHub Actions
|
|
8
|
-
for
|
|
31
|
+
for the content workflow on the commit SHA (Waiting / Exporting / Live /
|
|
9
32
|
Failed). Local Export keeps a real reexport button with the same sync colors.
|
|
10
33
|
- `GET /api/studio/pipeline-status?sha=` on the Studio Function
|
|
11
34
|
|
|
12
35
|
### Changed
|
|
13
36
|
|
|
14
|
-
- Studio docs: document
|
|
15
|
-
|
|
37
|
+
- Studio docs: document the pipeline indicator, Actions read on the PAT, and
|
|
38
|
+
toolbar states
|
|
16
39
|
|
|
17
40
|
## [1.4.3] — 2026-10-07
|
|
18
41
|
|
|
@@ -219,10 +219,10 @@ export async function listDir(env, dirPath) {
|
|
|
219
219
|
/**
|
|
220
220
|
* Map a GitHub Actions workflow run to Studio pipeline UI state.
|
|
221
221
|
* @param {{ status?: string, conclusion?: string|null }|null} run
|
|
222
|
-
* @returns {"awaiting_run"|"running"|"ok"|"error"}
|
|
222
|
+
* @returns {"awaiting_run"|"running"|"ok"|"error"|"idle"}
|
|
223
223
|
*/
|
|
224
224
|
export function mapWorkflowRunState(run) {
|
|
225
|
-
if (!run) return "
|
|
225
|
+
if (!run) return "idle";
|
|
226
226
|
const status = String(run.status || "");
|
|
227
227
|
if (status === "completed") {
|
|
228
228
|
return run.conclusion === "success" ? "ok" : "error";
|
|
@@ -231,23 +231,130 @@ export function mapWorkflowRunState(run) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
234
|
+
* Read pandorga.content.workflow(+ workflow_ref) from site `_config.yml`.
|
|
235
|
+
* Tiny indent-aware peek — no YAML dependency in the Function.
|
|
236
|
+
*/
|
|
237
|
+
export function parseContentWorkflowFromConfig(yamlText) {
|
|
238
|
+
const lines = String(yamlText || "").split(/\r?\n/);
|
|
239
|
+
let inPandorga = false;
|
|
240
|
+
let pandorgaIndent = 0;
|
|
241
|
+
let inContent = false;
|
|
242
|
+
let contentIndent = 0;
|
|
243
|
+
let workflow = null;
|
|
244
|
+
let workflowRef = null;
|
|
245
|
+
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
248
|
+
const m = line.match(/^(\s*)([A-Za-z0-9_]+):\s*(.*?)\s*$/);
|
|
249
|
+
if (!m) continue;
|
|
250
|
+
const indent = m[1].length;
|
|
251
|
+
const key = m[2];
|
|
252
|
+
let val = m[3];
|
|
253
|
+
if (
|
|
254
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
255
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
256
|
+
) {
|
|
257
|
+
val = val.slice(1, -1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!inPandorga) {
|
|
261
|
+
if (key === "pandorga" && indent === 0) {
|
|
262
|
+
inPandorga = true;
|
|
263
|
+
pandorgaIndent = indent;
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (indent <= pandorgaIndent && key !== "pandorga") break;
|
|
269
|
+
|
|
270
|
+
if (key === "content" && indent > pandorgaIndent) {
|
|
271
|
+
inContent = true;
|
|
272
|
+
contentIndent = indent;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (inContent) {
|
|
277
|
+
if (indent <= contentIndent) {
|
|
278
|
+
inContent = false;
|
|
279
|
+
if (key === "content" && indent > pandorgaIndent) {
|
|
280
|
+
inContent = true;
|
|
281
|
+
contentIndent = indent;
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (key === "workflow" && val) workflow = val;
|
|
286
|
+
if (key === "workflow_ref" && val) workflowRef = val;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { workflow, workflowRef };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Workflow file + branch: optional env override, else `_config.yml`
|
|
295
|
+
* (`pandorga.content.workflow`).
|
|
296
|
+
*/
|
|
297
|
+
export async function resolveContentWorkflowConfig(env) {
|
|
298
|
+
const envWorkflow = String(env.STUDIO_CONTENT_WORKFLOW || "").trim();
|
|
299
|
+
const envRef = String(env.STUDIO_CONTENT_WORKFLOW_REF || "").trim();
|
|
300
|
+
if (envWorkflow) {
|
|
301
|
+
return {
|
|
302
|
+
workflow: envWorkflow,
|
|
303
|
+
ref: envRef || "main",
|
|
304
|
+
source: "env",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const file = await getFile(env, "_config.yml");
|
|
310
|
+
if (!file?.content) {
|
|
311
|
+
return {
|
|
312
|
+
workflow: null,
|
|
313
|
+
ref: envRef || "main",
|
|
314
|
+
source: null,
|
|
315
|
+
error: "config_missing",
|
|
316
|
+
message: "_config.yml was not found on main.",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const parsed = parseContentWorkflowFromConfig(file.content);
|
|
320
|
+
if (parsed.workflow) {
|
|
321
|
+
return {
|
|
322
|
+
workflow: parsed.workflow,
|
|
323
|
+
ref: parsed.workflowRef || envRef || "main",
|
|
324
|
+
source: "config",
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
workflow: null,
|
|
329
|
+
ref: envRef || "main",
|
|
330
|
+
source: null,
|
|
331
|
+
error: "workflow_unset",
|
|
332
|
+
message:
|
|
333
|
+
"Set pandorga.content.workflow in _config.yml (e.g. content-pipeline.yml).",
|
|
334
|
+
};
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return {
|
|
337
|
+
workflow: null,
|
|
338
|
+
ref: envRef || "main",
|
|
339
|
+
source: null,
|
|
340
|
+
error: err.code || "config_read_failed",
|
|
341
|
+
message: String(err.message || err),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Look up the content-pipeline workflow run for a commit SHA, or the latest
|
|
348
|
+
* run on the branch when `sha` is omitted.
|
|
235
349
|
* Requires the PAT to have Actions read (fine-grained: Actions → Read).
|
|
236
350
|
*
|
|
237
351
|
* @param {object} env
|
|
238
|
-
* @param {string} sha
|
|
239
|
-
* @returns {Promise<{
|
|
240
|
-
* mode: "production",
|
|
241
|
-
* workflow: string|null,
|
|
242
|
-
* sha: string,
|
|
243
|
-
* state: "awaiting_run"|"running"|"ok"|"error"|"unconfigured",
|
|
244
|
-
* run: object|null,
|
|
245
|
-
* error: string|null
|
|
246
|
-
* }>}
|
|
352
|
+
* @param {string} [sha]
|
|
247
353
|
*/
|
|
248
354
|
export async function getContentPipelineStatus(env, sha) {
|
|
249
|
-
const
|
|
250
|
-
const
|
|
355
|
+
const resolved = await resolveContentWorkflowConfig(env);
|
|
356
|
+
const workflowFile = resolved.workflow;
|
|
357
|
+
const ref = resolved.ref || "main";
|
|
251
358
|
const commitSha = String(sha || "").trim().toLowerCase();
|
|
252
359
|
|
|
253
360
|
if (!workflowFile) {
|
|
@@ -257,11 +364,15 @@ export async function getContentPipelineStatus(env, sha) {
|
|
|
257
364
|
sha: commitSha,
|
|
258
365
|
state: "unconfigured",
|
|
259
366
|
run: null,
|
|
260
|
-
error:
|
|
367
|
+
error: resolved.error || "workflow_unset",
|
|
368
|
+
source: resolved.source,
|
|
369
|
+
message:
|
|
370
|
+
resolved.message ||
|
|
371
|
+
"Set pandorga.content.workflow in _config.yml (e.g. content-pipeline.yml).",
|
|
261
372
|
};
|
|
262
373
|
}
|
|
263
|
-
if (!/^[0-9a-f]{7,40}$/.test(commitSha)) {
|
|
264
|
-
const err = new Error("sha
|
|
374
|
+
if (commitSha && !/^[0-9a-f]{7,40}$/.test(commitSha)) {
|
|
375
|
+
const err = new Error("sha must be 7–40 hex chars when provided");
|
|
265
376
|
err.status = 400;
|
|
266
377
|
err.code = "invalid_sha";
|
|
267
378
|
throw err;
|
|
@@ -279,6 +390,7 @@ export async function getContentPipelineStatus(env, sha) {
|
|
|
279
390
|
state: "unconfigured",
|
|
280
391
|
run: null,
|
|
281
392
|
error: "workflow_not_found",
|
|
393
|
+
source: resolved.source,
|
|
282
394
|
};
|
|
283
395
|
}
|
|
284
396
|
if (wfRes.status === 401 || wfRes.status === 403) {
|
|
@@ -299,11 +411,13 @@ export async function getContentPipelineStatus(env, sha) {
|
|
|
299
411
|
}
|
|
300
412
|
|
|
301
413
|
const workflow = await wfRes.json();
|
|
302
|
-
|
|
414
|
+
let runsPath =
|
|
303
415
|
`/actions/workflows/${workflow.id}/runs` +
|
|
304
416
|
`?branch=${encodeURIComponent(ref)}` +
|
|
305
|
-
`&head_sha=${encodeURIComponent(commitSha)}` +
|
|
306
417
|
`&per_page=5`;
|
|
418
|
+
if (commitSha) {
|
|
419
|
+
runsPath += `&head_sha=${encodeURIComponent(commitSha)}`;
|
|
420
|
+
}
|
|
307
421
|
const runsRes = await gh(env, runsPath);
|
|
308
422
|
if (runsRes.status === 401 || runsRes.status === 403) {
|
|
309
423
|
const text = await runsRes.text();
|
|
@@ -325,12 +439,14 @@ export async function getContentPipelineStatus(env, sha) {
|
|
|
325
439
|
const payload = await runsRes.json();
|
|
326
440
|
const runs = Array.isArray(payload.workflow_runs) ? payload.workflow_runs : [];
|
|
327
441
|
const run = runs[0] || null;
|
|
328
|
-
|
|
442
|
+
let state = mapWorkflowRunState(run);
|
|
443
|
+
// Watching a specific SHA with no run yet → waiting for the push trigger.
|
|
444
|
+
if (commitSha && !run) state = "awaiting_run";
|
|
329
445
|
|
|
330
446
|
return {
|
|
331
447
|
mode: "production",
|
|
332
448
|
workflow: workflowFile,
|
|
333
|
-
sha: commitSha,
|
|
449
|
+
sha: commitSha || (run?.head_sha ? String(run.head_sha).toLowerCase() : ""),
|
|
334
450
|
state,
|
|
335
451
|
run: run
|
|
336
452
|
? {
|
|
@@ -342,6 +458,7 @@ export async function getContentPipelineStatus(env, sha) {
|
|
|
342
458
|
}
|
|
343
459
|
: null,
|
|
344
460
|
error: null,
|
|
461
|
+
source: resolved.source,
|
|
345
462
|
};
|
|
346
463
|
}
|
|
347
464
|
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -33,14 +33,20 @@ end
|
|
|
33
33
|
|
|
34
34
|
check!(gh_src.include?("getContentPipelineStatus"),
|
|
35
35
|
"github helper must expose getContentPipelineStatus")
|
|
36
|
-
check!(gh_src.include?("
|
|
37
|
-
"pipeline status must read
|
|
36
|
+
check!(gh_src.include?("parseContentWorkflowFromConfig"),
|
|
37
|
+
"pipeline status must read pandorga.content.workflow from _config.yml")
|
|
38
|
+
check!(gh_src.include?("resolveContentWorkflowConfig"),
|
|
39
|
+
"pipeline status must resolve workflow from config (env optional override)")
|
|
38
40
|
check!(gh_src.include?("mapWorkflowRunState"),
|
|
39
41
|
"pipeline status must map Actions run status to UI state")
|
|
40
42
|
check!(REPO_ROOT.join("studio-app/src/api/client.js").read.include?("pipelineStatus"),
|
|
41
43
|
"SPA client must call pipeline-status")
|
|
42
44
|
check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("armPipelineWatch"),
|
|
43
45
|
"SPA must watch the content pipeline after save")
|
|
46
|
+
check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("refreshPipelineStatus"),
|
|
47
|
+
"SPA must hydrate pipeline status on boot")
|
|
48
|
+
check!(!REPO_ROOT.join("studio-app/src/main.js").read.match?(/action:\s*"delete"/),
|
|
49
|
+
"toolbar must not include a Delete action")
|
|
44
50
|
|
|
45
51
|
check!(api_src.include?("media/folder") || api_src.include?('"folder"'),
|
|
46
52
|
"API router should expose media folder create")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* parseContentWorkflowFromConfig — reads pandorga.content.workflow from _config.yml.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { parseContentWorkflowFromConfig } from "../../functions/api/studio/_lib/github.js";
|
|
7
|
+
|
|
8
|
+
const sample = `# site
|
|
9
|
+
title: Example
|
|
10
|
+
|
|
11
|
+
pandorga:
|
|
12
|
+
# comment above content
|
|
13
|
+
content:
|
|
14
|
+
backend: r2
|
|
15
|
+
base_url: https://example.invalid
|
|
16
|
+
# Actions workflow Studio watches after Save
|
|
17
|
+
workflow: content-pipeline.yml
|
|
18
|
+
workflow_ref: main
|
|
19
|
+
identity:
|
|
20
|
+
name: Example
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const parsed = parseContentWorkflowFromConfig(sample);
|
|
24
|
+
assert.equal(parsed.workflow, "content-pipeline.yml");
|
|
25
|
+
assert.equal(parsed.workflowRef, "main");
|
|
26
|
+
|
|
27
|
+
const missing = parseContentWorkflowFromConfig("pandorga:\n identity:\n name: x\n");
|
|
28
|
+
assert.equal(missing.workflow, null);
|
|
29
|
+
|
|
30
|
+
const quoted = parseContentWorkflowFromConfig(
|
|
31
|
+
"pandorga:\n content:\n workflow: \"my-workflow.yml\"\n"
|
|
32
|
+
);
|
|
33
|
+
assert.equal(quoted.workflow, "my-workflow.yml");
|
|
34
|
+
|
|
35
|
+
console.log("All Studio pipeline config parse checks passed.");
|
data/scripts/validate.sh
CHANGED
|
@@ -68,6 +68,11 @@ for test in "${TESTS[@]}"; do
|
|
|
68
68
|
fi
|
|
69
69
|
done
|
|
70
70
|
|
|
71
|
+
echo "-- scripts/test/test-studio-pipeline-config.mjs"
|
|
72
|
+
if ! node scripts/test/test-studio-pipeline-config.mjs; then
|
|
73
|
+
failed=1
|
|
74
|
+
fi
|
|
75
|
+
|
|
71
76
|
if [[ "$failed" -ne 0 ]]; then
|
|
72
77
|
echo "VALIDATE FAILED"
|
|
73
78
|
exit 1
|