Reference
Workflows
The two reusable GitHub Actions workflows, docs-build.yml and docs-deploy.yml, with their inputs, permissions, steps and the trust model.
On this page
Two reusable workflows live in Avunu/docusystem/.github/workflows/. A project calls them from its own docs.yml and docs-publish.yml with:
uses: Avunu/docusystem/.github/workflows/<file>@<40-hex commit> # vX.Y.ZThe caller pins a commit, never a tag or a branch, and puts the version in the trailing comment. There is no moving major tag. Callers' triggers must be pull_request, push or workflow_dispatch, never pull_request_target or workflow_run, because the build workflow checks out and executes the pull request's code. docusystem doctor errors on both.
The setup a maintainer does once is in Publishing.
docs-build.yml
| Item | Value |
|---|---|
Input site-directory | String, default docs-site. Validated against ^[A-Za-z0-9_][A-Za-z0-9._/-]*$ with no .., and passed to shell steps only through the environment. |
Input pages-artifact | Boolean, default false. true uploads the Pages artifact (with hidden files) for docs-deploy.yml; otherwise a 7-day review artifact named docs-site. |
| Outputs, secrets | None |
| Job | Id check, name Check. From a caller job named build the required-check name is build / Check. On a push it runs only for the default branch. Permissions: contents: read. |
| Environment | DOCUSYSTEM_WORKFLOW_CONTRACT: "1" and DOCUSYSTEM_BRANCH, the repository's default branch |
Steps:
Validate the site directory.
Check out the repository with
persist-credentials: false.Require
package-lock.jsonand refusebun.lockandbun.lockb.Set up Node 24 with
package-manager-cache: false.npm ci --ignore-scripts --no-audit --no-fund.npm audit signatures, plus an assertion that@avunu/docusystemhas a verified provenance attestation.CI=true ./node_modules/.bin/docusystem check --ci: the installed CLI, nevernpxorbunx.Upload the site: the review artifact on a pull request or when
pages-artifactis false, else the Pages artifact.On failure, upload
.docusystem/manifest.json,.docusystem/jx.logand.docusystem/site(withoutnode_modules) as the artifactdocusystem-debug.
docs-deploy.yml
| Item | Value |
|---|---|
| Inputs, secrets | None |
Output page-url | The Pages URL |
| Job | Id deploy, name Publish. Runs only for events other than pull_request, on the default branch. Permissions: pages: write and id-token: write. Concurrency group pages with cancel-in-progress: false. Environment github-pages. |
| Steps | configure-pages, then deploy-pages |
It downloads the Pages artifact that docs-build.yml made in the same run and publishes it. It executes no project code. Every action in both workflows is pinned to a commit SHA.
Permissions
A called workflow cannot widen its caller's permissions, so the caller grants them:
docs.yml(pull requests) calls onlydocs-build.ymland grants onlycontents: read.docs-publish.ymlgrantscontents: readto the build job, andpages: writeandid-token: writeto the deploy job. It is the only file that grants write permissions.Every caller starts with
permissions: {}.
No workflow that a pull request can trigger asks for pages: write or id-token: write. That is why the system has two caller files, and why the reusable side is split the same way: it is not verified that a called workflow which asks for those permissions starts on a Dependabot-authored pull request, and Dependabot's upgrade pull requests are the main upgrade path.
Trust model
A pull request can change docs/ and docs-site/: the config, the lockfile and any override. An override's component JSON can carry compiler-time function bodies that run at build time, so a pull request that changes docs-site/ can run code in the build job. That job has a read-only token and no secrets. A pull request that changes only docs/ cannot: a page is data.
A pull request cannot:
change the reusable workflows, which are pinned by commit;
reach the deploy job, which is in a file that no pull request triggers;
publish anything.
A page is data because nothing a page says reaches Jx as a template. Jx evaluates every string that holds ${ as JavaScript, and its parser makes inert only the text of a page, not what becomes an attribute (a link address, a URL, a raw HTML attribute, a directive, the language of a fence) and not the strings that the layouts hand to components. So the build writes every ${ of a page inert itself, whatever the Markdown around it means: staging does it for the pages, the sidebar data is cleaned the same way, the configuration's text keys are refused when they hold one, and the catalog contract refuses one. The escaping of page titles and headings is kept. The seeded random corpus of test/integration/jx-expressions-fuzz.test.ts builds hostile pages with the real Jx and fails if any expression runs.
If a called deploy job is rejected
The one thing that could not be tested before the first real run is whether the deploy job of docs-publish.yml can call docs-deploy.yml with pages: write and id-token: write. If GitHub rejects it, the only change is in docs-publish.yml: replace the job that has uses: by a job with the same permissions, needs and if, whose two steps are the SHA-pinned configure-pages and deploy-pages actions copied from docs-deploy.yml. The build side and the package do not change. This shape is documented, not shipped.
The workflow contract
docs-build.yml sets DOCUSYSTEM_WORKFLOW_CONTRACT: "1". The CLI compares it with the integer WORKFLOW_CONTRACT it implements and exits 3 on a mismatch, naming the uses: line to change. The contract changes only when the workflows and the CLI stop being able to work together: the command the workflow runs (docusystem check --ci from the site folder), the environment it sets, or the output path <site>/dist. See Versioning.