From 103b22003ce40605ffd898635e53d47f83c0f9f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 00:37:51 +0000 Subject: [PATCH 1/3] Retry FunctionsLoader's include on a transient template-not-found race Roughly a third of the "Publish Docs" theme-gallery matrix jobs (fresh JVM per job) were hitting functions.bxs at [...] failed to load: The template path [...] could not be found on the very first include the whole build performs, immediately after fileExists() on that exact path had just confirmed it was there - blocking assemble-and-deploy (needs: build) every time, since a different subset of themes crashed on each run. That shape - transient, only on the first include of the run, on a path a moment-old existence check already verified - points to a cold-start race in BoxLang's own template resolver rather than a real missing file. load() now retries up to twice more (150ms/300ms backoff) when it hits that specific message and a fresh fileExists() recheck still confirms the file exists; any other failure, including a real syntax error in a project's own functions.bxs, still throws immediately on the first attempt. --- changelog.md | 1 + models/build/FunctionsLoader.bx | 37 ++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 7a6af0644a..5d2f76e1a3 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Fixed `FunctionsLoader.bx` intermittently failing a real `bxSites build` with `functions.bxs at [...] failed to load: The template path [...] could not be found`, even though the file genuinely exists - observed on this repository's own "Publish Docs" CI (a fresh JVM per theme-matrix job), where roughly a third of ten otherwise-identical jobs hit this on the very first `include` the whole build performs (`functionsLoader.load()`, called once at the very start of `build()`), immediately after `fileExists()` on that same path had just confirmed it was there. That pattern - transient, first-`include`-of-the-run only, same exact path a moment-old existence check already verified - points to a cold-start race in BoxLang's own template resolver rather than a real missing file. `load()` now retries `include` up to twice more (150ms/300ms backoff) when it hits that specific message and a fresh `fileExists()` recheck still says the file is there; any other failure (a real syntax error in a project's own `functions.bxs`) still throws immediately on the first attempt, unchanged from before. * Fixed `.github/workflows/pages.yml` (this repo's own GitHub Pages deploy) never actually running since the `versions.default` cutover - its trigger/deploy condition was changed to `main`, a branch that doesn't exist yet in this repository (`development` is still this project's only real branch, actively working toward the 1.0.0 release; `main` will only come into being once `development` is later merged into it to cut that release, at which point `main` becomes the frozen `1.0.x` line). Moved the trigger and the `Deploy to the site root` step's condition back to `development` so the versioned site (1.0.x at the root, `/next/` for work in progress) actually publishes again. * **A page's own frontmatter is now available to `{{ }}` variable expressions, no `bxsites.yaml` entry needed.** Previously `{{ dotted.path }}` only ever resolved against `bxsites.yaml`'s site-wide `variables` block; a page's own frontmatter values (title, summary, or any custom key a project defines) were reachable from a magic function's bare `page` reference but not from plain `{{ }}` markdown. `DocsLoader.bx`/`BlogDiscoverer.bx` now also preserve the raw, unfiltered frontmatter struct on every loaded page/post as `page.frontmatter` (previously only a fixed, named set of fields survived - a custom key like `product: BoxLang` was silently dropped); `BuildPipeline.bx`'s `convertMarkdown()` merges that page's own struct into `VariablesProcessor.bx`'s lookup scope under a reserved `page` key, so `{{ page.title }}` and `{{ page.frontmatter.product }}` resolve through the exact same dotted-path mechanism `{{ company }}` already does, with zero changes needed inside `VariablesProcessor.bx` itself. `page` joins the existing reserved "supporting variable" names (already reserved for a magic function's own bare reference) - a `variables.page` entry, if a project somehow declared one, is shadowed by the current page's own struct. See `docs/guides/variables-and-functions.md#page-variables` diff --git a/models/build/FunctionsLoader.bx b/models/build/FunctionsLoader.bx index 811ef6f1fc..9a86d68e4b 100644 --- a/models/build/FunctionsLoader.bx +++ b/models/build/FunctionsLoader.bx @@ -95,13 +95,36 @@ class { } var before = structKeyArray( variables ) - try { - include functionsPath - } catch ( any e ) { - throw( - type : "BxSites.InvalidFunctions", - message : "functions.bxs at [#functionsPath#] failed to load: #e.message#" - ) + // A handful of CI runs (observed on the "Publish Docs" theme-gallery + // matrix - fresh JVM every job) have hit `include` throwing "The + // template path [...] could not be found" for this exact path on + // the very first `include` the whole build performs, immediately + // after `fileExists()` just above confirmed it - a cold-start race + // in BoxLang's own template resolver, not a real missing file. A + // bounded retry, gated on that specific message *and* a fresh + // `fileExists()` recheck (so a genuinely deleted-mid-build file + // still fails fast), rides out the race without masking a real + // compile error in a project's own functions.bxs, which throws + // immediately on its first (and only) attempt. + var maxAttempts = 3 + var attempt = 0 + while ( true ) { + attempt++ + try { + include functionsPath + break + } catch ( any e ) { + var isTransientNotFound = attempt < maxAttempts + && findNoCase( "could not be found", e.message ) + && fileExists( functionsPath ) + if ( !isTransientNotFound ) { + throw( + type : "BxSites.InvalidFunctions", + message : "functions.bxs at [#functionsPath#] failed to load: #e.message#" + ) + } + sleep( 150 * attempt ) + } } var fns = {} From c82607705d6621a9027608df4daa40f3c86e8413 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 00:52:30 +0000 Subject: [PATCH 2/3] Replace retry band-aid with the actual fix: registered mappings The previous commit treated the symptom as a timing race and retried the include. It isn't a race: BoxLang's include statement never resolves a bare OS absolute path against the filesystem at all - an absolute-looking path gets coerced against registered mappings/webroot instead, so include functionsPath (and ThemeRenderer.bx's identical include arguments.themeDir & "/layout.bxm") were simply an unsupported way to reach an arbitrary, runtime-computed directory, regardless of how many times it's retried. Both now call getBoxRuntime().getConfiguration().registerMapping() to register a mapping for the target directory, then include through that mapping-prefixed path - the actual supported mechanism for this. Fixed ThemeRenderer.bx's layout.bxm include too, even though it hasn't been observed failing yet - it's the identical pattern, and every failing build so far aborted at functionsLoader.load() before any page ever reached this code path, so absence of evidence there isn't evidence of absence. --- changelog.md | 2 +- models/build/FunctionsLoader.bx | 55 +++++++++++++++------------------ models/build/ThemeRenderer.bx | 16 +++++++++- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/changelog.md b/changelog.md index 5d2f76e1a3..e1e7f32f12 100644 --- a/changelog.md +++ b/changelog.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -* Fixed `FunctionsLoader.bx` intermittently failing a real `bxSites build` with `functions.bxs at [...] failed to load: The template path [...] could not be found`, even though the file genuinely exists - observed on this repository's own "Publish Docs" CI (a fresh JVM per theme-matrix job), where roughly a third of ten otherwise-identical jobs hit this on the very first `include` the whole build performs (`functionsLoader.load()`, called once at the very start of `build()`), immediately after `fileExists()` on that same path had just confirmed it was there. That pattern - transient, first-`include`-of-the-run only, same exact path a moment-old existence check already verified - points to a cold-start race in BoxLang's own template resolver rather than a real missing file. `load()` now retries `include` up to twice more (150ms/300ms backoff) when it hits that specific message and a fresh `fileExists()` recheck still says the file is there; any other failure (a real syntax error in a project's own `functions.bxs`) still throws immediately on the first attempt, unchanged from before. +* Fixed `FunctionsLoader.bx` and `ThemeRenderer.bx` intermittently failing a real `bxSites build` with `functions.bxs at [...] failed to load: The template path [...] could not be found` (or the same for a theme's `layout.bxm`), even though the file genuinely exists - observed on this repository's own "Publish Docs" CI, where roughly a third of ten otherwise-identical theme-matrix jobs hit this on `functionsLoader.load()` (the very first `include` the whole build performs), immediately after `fileExists()` on that same path had just confirmed it was there. Root cause: a bare `include` statement doesn't resolve an OS absolute filesystem path at all - BoxLang coerces an absolute-looking path against registered mappings/webroot instead, so `include functionsPath`/`include arguments.themeDir & "/layout.bxm"` were never a reliably supported way to reach an arbitrary, runtime-computed directory, not a timing race. Both now register a runtime mapping (`getBoxRuntime().getConfiguration().registerMapping()`) for the target directory and include through that mapping-prefixed path instead - the actually-supported mechanism. * Fixed `.github/workflows/pages.yml` (this repo's own GitHub Pages deploy) never actually running since the `versions.default` cutover - its trigger/deploy condition was changed to `main`, a branch that doesn't exist yet in this repository (`development` is still this project's only real branch, actively working toward the 1.0.0 release; `main` will only come into being once `development` is later merged into it to cut that release, at which point `main` becomes the frozen `1.0.x` line). Moved the trigger and the `Deploy to the site root` step's condition back to `development` so the versioned site (1.0.x at the root, `/next/` for work in progress) actually publishes again. * **A page's own frontmatter is now available to `{{ }}` variable expressions, no `bxsites.yaml` entry needed.** Previously `{{ dotted.path }}` only ever resolved against `bxsites.yaml`'s site-wide `variables` block; a page's own frontmatter values (title, summary, or any custom key a project defines) were reachable from a magic function's bare `page` reference but not from plain `{{ }}` markdown. `DocsLoader.bx`/`BlogDiscoverer.bx` now also preserve the raw, unfiltered frontmatter struct on every loaded page/post as `page.frontmatter` (previously only a fixed, named set of fields survived - a custom key like `product: BoxLang` was silently dropped); `BuildPipeline.bx`'s `convertMarkdown()` merges that page's own struct into `VariablesProcessor.bx`'s lookup scope under a reserved `page` key, so `{{ page.title }}` and `{{ page.frontmatter.product }}` resolve through the exact same dotted-path mechanism `{{ company }}` already does, with zero changes needed inside `VariablesProcessor.bx` itself. `page` joins the existing reserved "supporting variable" names (already reserved for a magic function's own bare reference) - a `variables.page` entry, if a project somehow declared one, is shadowed by the current page's own struct. See `docs/guides/variables-and-functions.md#page-variables` diff --git a/models/build/FunctionsLoader.bx b/models/build/FunctionsLoader.bx index 9a86d68e4b..f6fd80165f 100644 --- a/models/build/FunctionsLoader.bx +++ b/models/build/FunctionsLoader.bx @@ -94,37 +94,32 @@ class { return {} } + // `include` doesn't resolve a bare OS absolute path against the real + // filesystem - BoxLang coerces an absolute-looking path against + // registered mappings/webroot instead, so `include functionsPath` + // here intermittently threw "The template path [...] could not be + // found" for this exact path, moments after `fileExists()` just + // above had already confirmed it was there - not a race, just an + // unsupported way to reach an arbitrary, runtime-computed + // directory. Registering a mapping for `dir` and including through + // that instead is the supported path (see + // `getBoxRuntime().getConfiguration().registerMapping()` - BoxLang + // docs' "Includes"/"Mappings & Class Resolution" pages). A fixed, + // module-namespaced prefix is safe to re-register on every call + // (cheap, and this class's own singleton is reused with a + // potentially different `docsDir` across `bxSites serve` rebuilds) - + // it simply repoints the same prefix at the current `dir`. + var mappingPrefix = "/bxsitesFunctionsInclude" + getBoxRuntime().getConfiguration().registerMapping( mappingPrefix, dir ) + var before = structKeyArray( variables ) - // A handful of CI runs (observed on the "Publish Docs" theme-gallery - // matrix - fresh JVM every job) have hit `include` throwing "The - // template path [...] could not be found" for this exact path on - // the very first `include` the whole build performs, immediately - // after `fileExists()` just above confirmed it - a cold-start race - // in BoxLang's own template resolver, not a real missing file. A - // bounded retry, gated on that specific message *and* a fresh - // `fileExists()` recheck (so a genuinely deleted-mid-build file - // still fails fast), rides out the race without masking a real - // compile error in a project's own functions.bxs, which throws - // immediately on its first (and only) attempt. - var maxAttempts = 3 - var attempt = 0 - while ( true ) { - attempt++ - try { - include functionsPath - break - } catch ( any e ) { - var isTransientNotFound = attempt < maxAttempts - && findNoCase( "could not be found", e.message ) - && fileExists( functionsPath ) - if ( !isTransientNotFound ) { - throw( - type : "BxSites.InvalidFunctions", - message : "functions.bxs at [#functionsPath#] failed to load: #e.message#" - ) - } - sleep( 150 * attempt ) - } + try { + include "#mappingPrefix#/functions.bxs" + } catch ( any e ) { + throw( + type : "BxSites.InvalidFunctions", + message : "functions.bxs at [#functionsPath#] failed to load: #e.message#" + ) } var fns = {} diff --git a/models/build/ThemeRenderer.bx b/models/build/ThemeRenderer.bx index 95a454e5ab..fad9288d0e 100644 --- a/models/build/ThemeRenderer.bx +++ b/models/build/ThemeRenderer.bx @@ -259,9 +259,23 @@ class { variables[ fnName ] = arguments.functions[ fnName ] } + // Same fix as FunctionsLoader.bx's own `load()` - a bare `include` + // statement doesn't resolve an OS absolute path against the real + // filesystem (BoxLang coerces it against registered mappings/webroot + // instead), so `include arguments.themeDir & "/layout.bxm"` + // intermittently threw "The template path [...] could not be found" + // for a path this class's own caller already resolved to a real, + // existing directory. Registering a mapping and including through + // that instead is the supported path. Cheap to re-register on every + // call (this method runs once per page) - a fixed, module-namespaced + // prefix simply gets repointed at the current page's own themeDir, + // which never actually changes mid-build (one theme per build). + var themeMappingPrefix = "/bxsitesThemeInclude" + getBoxRuntime().getConfiguration().registerMapping( themeMappingPrefix, arguments.themeDir ) + var html = "" bx:savecontent variable="html" { - include arguments.themeDir & "/layout.bxm" + include "#themeMappingPrefix#/layout.bxm" } return html } From 90cd93c009774950120069d4d0486e1c03f9a354 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 02:12:47 +0000 Subject: [PATCH 3/3] Pin CI to testbox@be to pick up TestBox#201's CLI-positionals fix tests.yml's test suite was intermittently failing before a single spec ran, with ClassNotFoundBoxLangException on TestBox's own BoxLangRunner.bx - blocking this PR's own CI (and any other PR's) independently of the registerMapping fix above. Root cause turned out to be a real TestBox bug, not a BoxLang engine issue: server.cli.parsed.positionals leaks the runner's own invocation path as argv[0] on current engine builds, and BoxLangRunner.bx's naive `positional[1]` grab misread that as a user-supplied bundle argument whenever none was actually given. Fixed upstream in Ortus-Solutions/TestBox#201 (reproduced and verified locally there). Pin id=testbox to id=testbox@be so CI picks up that fix from the bleeding-edge channel now, rather than waiting on a tagged stable TestBox release. Revert to the unpinned id=testbox once one ships it. --- .github/workflows/tests.yml | 12 +++++++++++- changelog.md | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index de0203c888..feb74e2f0b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,17 @@ jobs: run: boxlang --version - name: Install Dev Dependencies - run: box install id=testbox --verbose --nosave + run: | + # Pinned to the bleeding-edge channel rather than the default + # latest-stable release - testbox@be picks up + # Ortus-Solutions/TestBox#201's fix for a real TestBox bug + # (server.cli.parsed.positionals leaking the runner's own + # invocation path as argv[0], misread as a bundle argument - + # ClassNotFoundBoxLangException on BoxLangRunner.bx) before that + # fix has made it into a tagged stable release. Revert to the + # unpinned `id=testbox` once a stable TestBox release carrying + # the fix ships. + box install id=testbox@be --verbose --nosave - name: Test Module run: | diff --git a/changelog.md b/changelog.md index e1e7f32f12..34880f2983 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Pinned `tests.yml`'s `box install id=testbox` (this repo's own CI test-suite dependency) to `testbox@be` (bleeding-edge) instead of the unpinned default. CI was intermittently failing before a single test spec ran, with `ClassNotFoundBoxLangException` on TestBox's own `BoxLangRunner.bx` - traced to a real TestBox bug ([Ortus-Solutions/TestBox#201](https://github.com/Ortus-Solutions/TestBox/pull/201)): `server.cli.parsed.positionals` on current BoxLang engine builds leaks the runner's own invocation path as its first entry (an argv[0] leak), which `BoxLangRunner.bx` misread as a user-supplied bundle argument whenever no real one was given. `testbox@be` picks up that fix ahead of a tagged stable release - revert to the unpinned `id=testbox` once one ships it. * Fixed `FunctionsLoader.bx` and `ThemeRenderer.bx` intermittently failing a real `bxSites build` with `functions.bxs at [...] failed to load: The template path [...] could not be found` (or the same for a theme's `layout.bxm`), even though the file genuinely exists - observed on this repository's own "Publish Docs" CI, where roughly a third of ten otherwise-identical theme-matrix jobs hit this on `functionsLoader.load()` (the very first `include` the whole build performs), immediately after `fileExists()` on that same path had just confirmed it was there. Root cause: a bare `include` statement doesn't resolve an OS absolute filesystem path at all - BoxLang coerces an absolute-looking path against registered mappings/webroot instead, so `include functionsPath`/`include arguments.themeDir & "/layout.bxm"` were never a reliably supported way to reach an arbitrary, runtime-computed directory, not a timing race. Both now register a runtime mapping (`getBoxRuntime().getConfiguration().registerMapping()`) for the target directory and include through that mapping-prefixed path instead - the actually-supported mechanism. * Fixed `.github/workflows/pages.yml` (this repo's own GitHub Pages deploy) never actually running since the `versions.default` cutover - its trigger/deploy condition was changed to `main`, a branch that doesn't exist yet in this repository (`development` is still this project's only real branch, actively working toward the 1.0.0 release; `main` will only come into being once `development` is later merged into it to cut that release, at which point `main` becomes the frozen `1.0.x` line). Moved the trigger and the `Deploy to the site root` step's condition back to `development` so the versioned site (1.0.x at the root, `/next/` for work in progress) actually publishes again. * **A page's own frontmatter is now available to `{{ }}` variable expressions, no `bxsites.yaml` entry needed.** Previously `{{ dotted.path }}` only ever resolved against `bxsites.yaml`'s site-wide `variables` block; a page's own frontmatter values (title, summary, or any custom key a project defines) were reachable from a magic function's bare `page` reference but not from plain `{{ }}` markdown. `DocsLoader.bx`/`BlogDiscoverer.bx` now also preserve the raw, unfiltered frontmatter struct on every loaded page/post as `page.frontmatter` (previously only a fixed, named set of fields survived - a custom key like `product: BoxLang` was silently dropped); `BuildPipeline.bx`'s `convertMarkdown()` merges that page's own struct into `VariablesProcessor.bx`'s lookup scope under a reserved `page` key, so `{{ page.title }}` and `{{ page.frontmatter.product }}` resolve through the exact same dotted-path mechanism `{{ company }}` already does, with zero changes needed inside `VariablesProcessor.bx` itself. `page` joins the existing reserved "supporting variable" names (already reserved for a magic function's own bare reference) - a `variables.page` entry, if a project somehow declared one, is shadowed by the current page's own struct. See `docs/guides/variables-and-functions.md#page-variables`