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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e883077823ea13efc6672caf40765fd68d30d10f5c28e7770a91a7859159511f
4
- data.tar.gz: 9fca02e64c54d5d1d5ef27b8d57ca907c852d09e8197d4fdf5125a862db84aca
3
+ metadata.gz: a8ea9638b4cbf0679fd3eeb328969878e5c4e7080be6a338257ccfc831f66213
4
+ data.tar.gz: 865776c26a7094cdf151ff21c6ecf492dd9e91171a6608bb23567323ff6ef8a2
5
5
  SHA512:
6
- metadata.gz: f3c860025a9557c736cbc3157a195d0e3c1fa0036d6eb944d99699326fb5f88f87ebe7891841a2fd870d56057a978dc00bb0b05b07f420bf0d9a3229874b675e
7
- data.tar.gz: 6778385016612559491561009245a8f248a037b77ce0ddadbf7702b159c729e128f87b896be49151973a177d9829910d8f7254e61319cd365ed6a3d4dd501e1b
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 `STUDIO_CONTENT_WORKFLOW` on the commit SHA (Waiting / Exporting / Live /
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 `STUDIO_CONTENT_WORKFLOW`, Actions read on the PAT, and
15
- the toolbar indicator states
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 "awaiting_run";
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
- * Look up the content-pipeline workflow run for a commit SHA.
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 workflowFile = String(env.STUDIO_CONTENT_WORKFLOW || "").trim();
250
- const ref = String(env.STUDIO_CONTENT_WORKFLOW_REF || "main").trim() || "main";
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: null,
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 required (7–40 hex chars)");
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
- const runsPath =
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
- const state = mapWorkflowRunState(run);
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.4.4"
4
+ VERSION = "1.4.6"
5
5
  end
@@ -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?("STUDIO_CONTENT_WORKFLOW") || api_src.include?("STUDIO_CONTENT_WORKFLOW"),
37
- "pipeline status must read STUDIO_CONTENT_WORKFLOW")
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