@norskvideo/ctl-dev-kit 0.2.23 → 0.2.25

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.
@@ -186,7 +186,14 @@ jobs:
186
186
  for attempt in 1 2 3 4 5; do
187
187
  if git push origin HEAD:main; then exit 0; fi
188
188
  echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
189
- git fetch origin main
189
+ # --unshallow, not a plain fetch: these runners keep their .git
190
+ # between jobs, and a commit an earlier job fetched at depth 1 stays
191
+ # recorded as parentless. A plain fetch does not re-fetch an object it
192
+ # already holds, so main's tip can look like an unrelated root and the
193
+ # rebase replays our base commit over it -- add/add conflicts on every
194
+ # file it touches (playout + studio, 2026-10-03). The fallback covers
195
+ # a repo that is already complete, where --unshallow is refused.
196
+ git fetch --unshallow origin main 2>/dev/null || git fetch origin main
190
197
  git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
191
198
  done
192
199
  echo "push still failing after 5 rebase attempts"; exit 1
@@ -318,7 +318,14 @@ jobs:
318
318
  for attempt in 1 2 3 4 5; do
319
319
  if git push origin HEAD:main; then exit 0; fi
320
320
  echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
321
- git fetch origin main
321
+ # --unshallow, not a plain fetch: these runners keep their .git
322
+ # between jobs, and a commit an earlier job fetched at depth 1 stays
323
+ # recorded as parentless. A plain fetch does not re-fetch an object it
324
+ # already holds, so main's tip can look like an unrelated root and the
325
+ # rebase replays our base commit over it -- add/add conflicts on every
326
+ # file it touches (playout + studio, 2026-10-03). The fallback covers
327
+ # a repo that is already complete, where --unshallow is refused.
328
+ git fetch --unshallow origin main 2>/dev/null || git fetch origin main
322
329
  git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
323
330
  done
324
331
  echo "push still failing after 5 rebase attempts"; exit 1
@@ -265,6 +265,28 @@ export function lockfileRefreshCanFail(yaml: string): number[] {
265
265
  return out;
266
266
  }
267
267
 
268
+ /** `- name:` lines of steps that rebase onto a fetched branch without first
269
+ * unshallowing.
270
+ *
271
+ * The self-hosted runners keep each repo's .git between jobs, and
272
+ * actions/checkout fetches at depth 1, so a commit an earlier job fetched is
273
+ * left recorded as parentless. A plain `git fetch` will not re-fetch an object
274
+ * it already holds, so when that commit is main's tip the rebase sees two
275
+ * unrelated roots and replays our base commit over it: add/add conflicts on
276
+ * every file, read as "a real conflict". It failed playout's sync-dev-kit and
277
+ * studio's upgrade-latest that way on 2026-10-03. `--unshallow` re-fetches the
278
+ * parents and clears the stale boundary. */
279
+ export function rebaseWithoutUnshallow(yaml: string): number[] {
280
+ const lines = yaml.split("\n");
281
+ const out: number[] = [];
282
+ lines.forEach((text, i) => {
283
+ if (!/^\s+- name:/.test(text)) return;
284
+ const body = stepBody(lines, i);
285
+ if (/\bgit rebase\b/.test(body) && !/git fetch --unshallow\b/.test(body)) out.push(i + 1);
286
+ });
287
+ return out;
288
+ }
289
+
268
290
  /** A `ctl-candidate` validation that another candidate, or its own retry, can
269
291
  * cancel.
270
292
  *
@@ -62,6 +62,19 @@ export declare function livePictureFailure(selector: string, phase: string | nul
62
62
  export declare function awaitLivePicture(page: Page, selector: string, opts?: {
63
63
  timeoutMs?: number;
64
64
  }): Promise<void>;
65
+ /** How long a capture waits for web fonts before shooting anyway. */
66
+ export declare const FONTS_READY_TIMEOUT_MS = 10000;
67
+ /** `document.fonts.ready`, bounded. True when the fonts settled in time.
68
+ *
69
+ * Unbounded, this hung commentary's composition guide for its whole 360s test
70
+ * budget on 2026-10-03 (CI only, after the Take, with the page about to render
71
+ * the advanced compositor): `fonts.ready` resolves only once every pending font
72
+ * load has finished, a request that never answers never finishes, and
73
+ * `page.evaluate` has no timeout of its own. The test then timed out, its
74
+ * teardown with it, and the instance ran on for hours. A capture in a fallback
75
+ * font is a far smaller failure than that, so bound the wait inside the page
76
+ * AND around the call -- a wedged renderer never answers the call at all. */
77
+ export declare function awaitFontsReady(page: Pick<Page, "evaluate">, timeoutMs?: number): Promise<boolean>;
65
78
  /** Produce the screenshot bytes for a capture, applying its treatment. Split out
66
79
  * from `DocGuide.capture` (which also gates on DOCS_GENERATE and writes the file)
67
80
  * so the treatment geometry is testable on its own. */
@@ -79,14 +79,47 @@ async function spotlightOn(page, selector, pad) {
79
79
  async function spotlightOff(page) {
80
80
  await page.evaluate((id) => document.getElementById(id)?.remove(), SPOTLIGHT_ID);
81
81
  }
82
+ /** How long a capture waits for web fonts before shooting anyway. */
83
+ export const FONTS_READY_TIMEOUT_MS = 10_000;
84
+ /** `document.fonts.ready`, bounded. True when the fonts settled in time.
85
+ *
86
+ * Unbounded, this hung commentary's composition guide for its whole 360s test
87
+ * budget on 2026-10-03 (CI only, after the Take, with the page about to render
88
+ * the advanced compositor): `fonts.ready` resolves only once every pending font
89
+ * load has finished, a request that never answers never finishes, and
90
+ * `page.evaluate` has no timeout of its own. The test then timed out, its
91
+ * teardown with it, and the instance ran on for hours. A capture in a fallback
92
+ * font is a far smaller failure than that, so bound the wait inside the page
93
+ * AND around the call -- a wedged renderer never answers the call at all. */
94
+ export async function awaitFontsReady(page, timeoutMs = FONTS_READY_TIMEOUT_MS) {
95
+ let timer;
96
+ const late = new Promise((resolve) => {
97
+ timer = setTimeout(() => resolve(false), timeoutMs);
98
+ });
99
+ const ready = page
100
+ .evaluate((ms) => Promise.race([
101
+ document.fonts.ready.then(() => true),
102
+ new Promise((resolve) => setTimeout(() => resolve(false), ms)),
103
+ ]), timeoutMs)
104
+ .catch(() => false);
105
+ try {
106
+ return await Promise.race([ready, late]);
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
82
112
  /** Produce the screenshot bytes for a capture, applying its treatment. Split out
83
113
  * from `DocGuide.capture` (which also gates on DOCS_GENERATE and writes the file)
84
114
  * so the treatment geometry is testable on its own. */
85
115
  export async function screenshotFor(page, treat) {
86
116
  if (treat?.livePicture)
87
117
  await awaitLivePicture(page, treat.livePicture, { timeoutMs: treat.livePictureTimeoutMs });
88
- // Wait for web fonts, else the snapshot catches a fallback font mid-swap.
89
- await page.evaluate(() => document.fonts.ready);
118
+ // Wait for web fonts, else the snapshot catches a fallback font mid-swap --
119
+ // but not forever (see awaitFontsReady).
120
+ if (!(await awaitFontsReady(page))) {
121
+ console.warn(`[doc-guide] web fonts not ready after ${FONTS_READY_TIMEOUT_MS}ms; capturing with what has loaded`);
122
+ }
90
123
  if (treat?.crop)
91
124
  return page.locator(treat.crop).first().screenshot();
92
125
  if (treat?.spotlight) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",