docs-site
Recipe card from the charly-tools plugin (Images — the deployable catalog).
docs-site — build the opencharly.ai site inside a box
Section titled “docs-site — build the opencharly.ai site inside a box”Audience: this page describes maintenance the charly repository performs on itself.
docs-site builds this documentation site, so every task … command below names a
gate this project runs over its own sources from a charly checkout — never a step a
reader runs. They are named because the candy’s contract is what keeps the published
site honest, not because you invoke them.
Candy Properties
Section titled “Candy Properties”| Property | Value |
|---|---|
| Requires | nodejs |
| Packages | git (fedora section) |
| Builds into | /srv/docs (source), /srv/docs/dist (built site) |
| Consumed by | the docs-site-app box → the check-docs bed |
docs-site clones the published opencharly/docs repository, runs npm ci against its
committed lockfile, and runs the production Astro build. A build failure — a bad frontmatter
scalar, a Starlight config a version bump invalidated, a page whose markdown will not parse —
fails the image build, from the same input Cloudflare Pages builds, so the box catches it
before a deploy can.
Why it clones the published repo
Section titled “Why it clones the published repo”The docs repo is standalone — charly no longer carries it as a submodule — and the candy cannot read another directory even if it did. The escapes are closed by design:
copy:rejects..—copy: "../../docs/x" may not contain .. (no traversal)at validate.- There is no field for pointing a candy at another directory. (An older revision of
/charly-image:layerdocumented adirectory:field for exactly this; that field does not exist inspec/schema/candy.cue, in the generated spec types, or in the loader, andcharly box validaterejects it asfield not allowed.)
Cloning the published repo is the schema-legal path, and it tests the more useful thing: the exact source Cloudflare builds.
Ordering consequence. The candy fetches an IMMUTABLE COMMIT, never a branch, so DOCS_REF
must be re-pinned to each new docs merge before this box — and the check-docs bed above it —
prove the new content. charly task docs-pin REPORTS that state: it fetches ls-remote and
compares DOCS_REF against the docs repo’s current main head, and a pin that is behind
is reported and stays green — charly has no docs gitlink anymore (the docs repo is
standalone, and this candy’s pin is the only thing that fixes what the bed tests), so the
advance is available NOW in opencharly/docs and this repo’s pin moves when someone takes
it. A report that reddened the gate on every docs merge that has not been re-pinned here
would be a nuisance that gets bypassed; the org pin contract draws the same line — a pin
that is behind is REPORTED, a pin the workflow cannot JUDGE is a FAILURE.
That re-pin is a CONFIG edit and changes the landing’s gate and attribution tier — see
/charly-internals:git-workflow (B6a step 4, and B2 “the derivation stops at the gitlink” for
the general rule), which owns landing mechanics.
The pin is TWO occurrences, and both are checked. The sha appears once as DOCS_REF
(the clone’s cache key) and once as a LITERAL in the docs-site-pinned-commit check’s
contains: matcher. The duplication is deliberate and cannot be collapsed: a check step’s
substitution does not resolve a candy var:, so writing ${DOCS_REF} in the matcher makes
charly SKIP the step with “unresolved variables: DOCS_REF” — turning the anti-staleness guard
into a non-event while the bed still reports PASS, which is the exact failure the guard exists
to catch. charly task docs-pin therefore checks BOTH, and it draws the line between them by
what each one MEANS. The currency of the pin — is DOCS_REF the docs repo’s current
main head — is REPORTED, because a pin that is behind is a state and the advance lives in
another repo. The agreement of the two occurrences — does the sha occur at least twice in
charly.yml — FAILS, because that is this tree contradicting itself, and a re-pin that
updated only one of them would leave the check asserting a commit nobody builds. Re-pinning
means editing both.
A branch name here is a correctness bug, not a convenience: var: values are emitted as ENV
above the steps, so with main the clone layer’s cache key never moves and the bed silently
rebuilds whatever commit that layer first captured. That shipped once, and every run passed while
proving stale content until review caught it.
| Var | Default |
|---|---|
DOCS_REPO |
https://github.com/opencharly/docs.git |
DOCS_REF |
the docs repo commit this bed tests — an immutable commit, never a branch (see above) |
The pin report — charly task docs-pin
Section titled “The pin report — charly task docs-pin”The bed proves the IMAGE holds the pin; nothing in the org proved the pin is the docs repo’s CURRENT head, and nothing proved the two occurrences agree. That control lives HERE — this repo owns the pin, and the control that reads it is a task, not a candy check, so it runs over this repo’s own sources rather than inside the image.
charly task docs-pinIt reads DOCS_REF and DOCS_REPO from this repo’s charly.yml, fetches the docs repo’s
main head with git ls-remote, and reports one of four states:
| State | Meaning | Exit |
|---|---|---|
| current | DOCS_REF IS the docs repo’s main head, and both occurrences are present |
0 |
| BEHIND | the pin is not the current head — the advance is available NOW in opencharly/docs |
0 |
| DISAGREE | the sha does not occur at least twice in charly.yml — this tree contradicts itself |
1 |
| UNJUDGEABLE | the head could not be fetched, or no DOCS_REF is declared |
1 |
scripts/docs-pin-report.sh owns the logic, and --self-test proves each state
discriminates against fixtures. Each case asserts the state’s own REPORT, not only an
exit status: current and BEHIND both exit 0, so only the message tells them apart.
The task runs both.
Every report NAMES the state it is in, so the state is greppable in the output — and the self-test proves this table stays the report’s own list: the count the sentence above claims, the rows above, and the states the script declares are asserted equal, and four mutations (a wrong count, a missing row, a row this script never declares, and a state this script declares that this table does not name) each turn the check red. A fifth state therefore cannot be added silently, in either direction.
Verification
Section titled “Verification”Every check is build-context, so charly check box docs-site-app proves the whole site builds
and has the right shape without deploying anything.
| Check | Asserts |
|---|---|
docs-site-node-present |
node reports a parseable major version |
docs-site-index-built |
/srv/docs/dist/index.html exists |
docs-site-cli-page |
a generated CLI reference page rendered |
docs-site-candy-acceptance-plan |
a candy page publishes its plan: as an acceptance spec |
docs-site-recipe-card |
a recipe card rendered with its cross-references rewritten to site links |
docs-site-runtime-plugin-page |
the load-bearing one — see below |
docs-site-provider-index |
the provider cross-index rendered |
The load-bearing check
Section titled “The load-bearing check”docs-site-runtime-plugin-page reads the page of plugin-cdp, which is not in
compiled_plugins: — it loads out-of-process over gRPC — and asserts both its rendered placement
and its CUE parameter schema.
That step is what fails if the generator ever narrows to documenting only the default-active set (the binary’s own command model, or the enabled-boxes list). It is deliberately the assertion that breaks first, because silently omitting everything that is not compiled in is the most plausible way for this catalog to go quietly wrong.
How the pages are located
Section titled “How the pages are located”The two corpus-projected pages — docs-site-candy-acceptance-plan and
docs-site-runtime-plugin-page — are resolved by name, not by a literal path. The site
publishes one page per corpus version, under a version-stamped directory
(<repo>-<name>-v<calver>/), so a literal path would redden the bed on the next corpus
advance — a false alarm about a change that never touched the site. Each resolves the newest
version and fails when no page exists at all: a generator that narrowed to documenting
nothing would otherwise leave cat reading an empty path, and the check would pass on an
empty stdout.
The unversioned paths this bed used to assert (/srv/docs/dist/reference/candy/charly/ and
/srv/docs/dist/reference/plugin/plugin-cdp/) are gone from the site: the docs repo dropped
them in its own #106, and the bed stayed green only because DOCS_REF was still pinned to a
docs commit from before that change. A stale pin can hide a broken assertion, so the pin must
be the docs repo’s current main head.
The check-docs bed
Section titled “The check-docs bed”charly check run check-docsA disposable: true pod deploy of docs-site-app. Reaching steady state proves the box builds,
starts and stays up; the shape assertions already ran at image build.
The generator’s own cross-reference integrity gate (an unresolvable
/charly-<plugin>:<skill> reference fails generation) is covered by the plugin’s Go tests —
this bed covers the artifact those produce. The committed content’s staleness against the
pinned charly is gated by the docs repo’s own deploy workflow, not here.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
docs-site-pinned-commit FAILS |
the clone layer is stale, or DOCS_REF was re-pinned without rebuilding |
charly task docs-pin REPORTS “DOCS_REF is BEHIND” |
the pin is not the docs repo’s current main head — the advance is available NOW in opencharly/docs; re-pin BOTH occurrences in charly.yml to take it. The report is green on purpose: a pin that is behind is a state, not a defect |
charly task docs-pin FAILS |
the two occurrences of the sha in charly.yml do not AGREE (the docs-site-pinned-commit matcher still names an older commit), or the docs repo’s head could not be fetched, so the pin’s currency is UNJUDGEABLE. The first is this tree contradicting itself — fix the matcher; the second is a gate that cannot judge, never a pass |
fetch fails upload-pack: not our ref <sha> |
DOCS_REF names a commit not reachable from ANY ref in the docs repo — never pushed, or garbage-collected |
fetch fails couldn't find remote ref <x> |
DOCS_REF is not a FULL 40-hex sha — either a ref name (which the pinned-commit contract forbids) or an abbreviated sha, which git fetch rejects even when the commit exists |
npm ci fails on a lockfile mismatch |
package.json and package-lock.json disagree — regenerate the lockfile in the docs repo |
a shape check fails but index.html exists |
the generator emitted a different tree layout; regenerate in the docs repo (its deploy workflow) and check the diff in a docs PR |
Cross-References
Section titled “Cross-References”/charly-build:docs— thecharly docs generateverb that produces the content this builds./charly-coder:nodejs— the node runtime this candy requires./charly-check:check— the bed model and the check-verb catalog.
When to Use This Skill
Section titled “When to Use This Skill”Invoke when working with the docs-site candy, the docs-site-app box, the check-docs bed, or
when the documentation site fails to build.

