From 781e4059f7354aecc5b089d83af2481b15ad764d Mon Sep 17 00:00:00 2001 From: Dieter Baier Date: Sun, 12 Jul 2026 12:59:34 +0200 Subject: [PATCH 1/2] Provide a docs-toolbox build script for consuming projects Consuming projects had no guided way to run the vendored validators, generators, and documentation render in the reproducible docs-toolbox image: the bootstrap skill and the "Consuming This Toolkit From a Project" guidance never mentioned a build script, the example project shipped none, and the toolkit's own root build.sh is wired to the toolkit itself. - Add templates/scripts/build.sh: a generic, parameterizable docs-toolbox task runner (validate, generate, adapters, check-adapters, build, all, clean) that a project copies to its root. Container-by-default with a DOCS_TOOLBOX_LOCAL host fallback; SOURCE_DOC/image are overridable. It wraps only the docs toolchain and leaves application builds on their own tooling. - Reference it from skills/bootstrap-project/SKILL.md (generator-support list, Reference-Don't-Copy copied-tooling list, and the complete-bootstrap list) and from the README "Consuming This Toolkit From a Project" section (copied vs. referenced, and a wiring step), mirroring the existing build-agent-adapters.js guidance: copy the templates/scripts/ version, not the toolkit's own root build.sh. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 20 ++- skills/bootstrap-project/SKILL.md | 7 ++ templates/scripts/build.sh | 201 ++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 4 deletions(-) create mode 100755 templates/scripts/build.sh diff --git a/README.md b/README.md index c088401..c9d7308 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,17 @@ the lookup order in the project's `AGENTS.md`. - **Copied / vendored into the project** — executable tooling that must run in the project's own build and CI: metamodel schemas (`metamodel/`), AsciiDoc - templates (`templates/`), validator/generator scripts (`scripts/`), and the - generic agent adapter generator from `templates/scripts/`. Keep these in sync + templates (`templates/`), validator/generator scripts (`scripts/`), the + generic agent adapter generator from `templates/scripts/`, and the generic + docs-toolbox task runner `templates/scripts/build.sh` (runs the validators, + generators, and Asciidoctor render inside the pinned docs-toolbox image so + local and CI runs share one toolchain). Keep these in sync with the toolkit; do not fork their behavior silently. The `bootstrap-project` skill copies them when a project has none. Do not copy the toolkit's own - `scripts/build-agent-adapters.js`; it is wired to the toolkit itself. Use the - parameterizable `templates/scripts/build-agent-adapters.js` instead. + `scripts/build-agent-adapters.js` or root `build.sh`; they are wired to the + toolkit itself. Use the parameterizable + `templates/scripts/build-agent-adapters.js` and `templates/scripts/build.sh` + instead. - **Referenced, never copied** — the architecture *guidance*: toolkit `skills/**/SKILL.md`, `features/`, and the toolkit's own contract text. Agents resolve these from the toolkit at need. @@ -140,6 +145,13 @@ silently re-state toolkit rules. directory name) and auto-detects whether to list local skills or route to the toolkit. Run `node scripts/check-agent-adapters.js` in CI to fail on stale adapters. +4. Copy the docs-toolbox task runner from `templates/scripts/build.sh` to the + project root and run architecture tasks through it — `./build.sh validate`, + `./build.sh generate`, `./build.sh build` — so validation, generation, and + the Asciidoctor render all run in the pinned docs-toolbox image locally and + in CI. Set `DOCS_TOOLBOX_LOCAL=1` to fall back to the host toolchain, and + adjust `SOURCE_DOC` if the arc42 entry document is not + `src/docs/doc-001-arc42.adoc`. A project that has **no** local architecture skills of its own (for example a tooling repository) still references the toolkit for all architecture and SDLC diff --git a/skills/bootstrap-project/SKILL.md b/skills/bootstrap-project/SKILL.md index 0f269dd..54d4d45 100644 --- a/skills/bootstrap-project/SKILL.md +++ b/skills/bootstrap-project/SKILL.md @@ -62,6 +62,7 @@ required: - copy templates; - copy metamodel schemas; - copy validation scripts; +- copy the docs-toolbox task runner (`templates/scripts/build.sh`); - copy documentation build configuration. If generators are unavailable: @@ -104,6 +105,10 @@ is copied or vendored and kept in sync with the toolkit: - the generic agent adapter generator from `templates/scripts/build-agent-adapters.js` and `templates/scripts/check-agent-adapters.js` (not the toolkit's own `scripts/build-agent-adapters.js`, which is wired to the toolkit itself); +- the generic docs-toolbox task runner from `templates/scripts/build.sh` (not + the toolkit's own root `build.sh`, which is wired to the toolkit itself); it + runs the validators, generators, and Asciidoctor render in the pinned + docs-toolbox image; - documentation build configuration. The target project's own `AGENTS.md`, `.github/copilot-instructions.md`, and @@ -225,6 +230,8 @@ the target repository. A complete bootstrap therefore includes, when required: - `templates/adr.adoc`; - `templates/quality-scenario.adoc`; - `templates/risk.adoc`; +- `build.sh` copied from `templates/scripts/build.sh` (docs-toolbox task runner + for `validate`, `generate`, and `build`); - documentation build or generator configuration available from the toolkit. After source artifacts are created or migrated, validate the architecture source diff --git a/templates/scripts/build.sh b/templates/scripts/build.sh new file mode 100755 index 0000000..7767d61 --- /dev/null +++ b/templates/scripts/build.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env sh +set -eu + +# Generic architecture-documentation task runner for projects that consume the +# architecture-knowledge-toolkit. Copy this file to the consuming project root +# (as ./build.sh) alongside the vendored metamodel/, templates/, and scripts/. +# +# By default every task runs inside a pinned docs-as-code-toolkit `docs-toolbox` +# container image, so local runs and CI use the same reproducible toolchain +# (Ruby, Node.js, Asciidoctor, PlantUML, Graphviz). The reproducibility +# guarantee holds only in this container mode. +# +# This wraps the vendored architecture validators and generators. It does NOT +# build application code (for example a JDK/Gradle or npm project); the +# docs-toolbox image only carries the documentation toolchain. Keep the +# application build on its own tooling. +# +# Set DOCS_TOOLBOX_LOCAL=1 to run a task against the host toolchain instead +# (whatever Ruby/Node is installed locally). If no container engine is available +# and local mode was not requested, the task aborts rather than silently using +# an unpinned host toolchain. +# +# Adjust SOURCE_DOC below if the project's arc42 entry document is not the +# default src/docs/doc-001-arc42.adoc. Modeled on the toolkit's own build.sh. + +COMMAND="${1:-build}" +if [ "$#" -gt 0 ]; then + shift +fi + +# Pin the image tag for reproducibility. Override with DOCS_TOOLBOX_IMAGE, for +# example to a digest for an even stricter pin. +DOCS_TOOLBOX_IMAGE="${DOCS_TOOLBOX_IMAGE:-ghcr.io/docs-as-code-toolkit/docs-toolbox:v1.3.1}" +BUILD_DIR="${BUILD_DIR:-build/architecture}" +SOURCE_DOC="${SOURCE_DOC:-src/docs/doc-001-arc42.adoc}" + +usage() { + cat <<'USAGE' +Usage: ./build.sh [args] + +Tasks: + validate Validate architecture artifact metadata and relations. + generate Validate, then generate derived AsciiDoc fragments/indexes. + adapters Regenerate the agent adapters (scripts/build-agent-adapters.js). + check-adapters Fail if the generated agent adapters are out of date. + build Generate fragments and render the architecture HTML. + all Run check-adapters and build (build also validates+generates). + clean Remove local architecture build output. + help Show this help. + +Execution modes: + Container (default) Runs inside the pinned docs-toolbox image (Docker/Podman). + This is the reproducible mode. + Local Set DOCS_TOOLBOX_LOCAL=1 to run against the host toolchain. + Not reproducible; uses whatever Ruby/Node is installed. + +With neither a container engine nor DOCS_TOOLBOX_LOCAL=1, the task aborts. +Override the image with DOCS_TOOLBOX_IMAGE. Application code is built with its +own tooling, not this script (docs-toolbox carries only the docs toolchain). +USAGE +} + +find_engine() { + if command -v podman >/dev/null 2>&1 && podman info >/dev/null 2>&1; then + printf '%s\n' "podman" + elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + printf '%s\n' "docker" + else + printf '%s\n' "" + fi +} + +run_local_validate() { + ruby scripts/validate-metamodel.rb +} + +run_local_generate() { + ruby scripts/validate-metamodel.rb --generate +} + +run_local_adapters() { + node scripts/build-agent-adapters.js +} + +run_local_check_adapters() { + node scripts/check-agent-adapters.js +} + +run_local_build() { + run_local_generate + mkdir -p "$BUILD_DIR" + asciidoctor \ + -r asciidoctor-diagram \ + -r asciidoctor-diagram/plantuml \ + --failure-level=ERROR \ + -a skip-front-matter \ + -a toc=left \ + -a sectanchors \ + -a icons=font \ + -a imagesdir=. \ + -a imagesoutdir="$BUILD_DIR" \ + -D "$BUILD_DIR" \ + -o index.html \ + "$SOURCE_DOC" + echo "Built architecture HTML: $BUILD_DIR/index.html" +} + +# `build` already runs generate (and therefore validate), so `all` does not +# validate separately to avoid a redundant pass. +run_local_all() { + run_local_check_adapters + run_local_build +} + +run_local() { + case "$COMMAND" in + validate) run_local_validate ;; + generate) run_local_generate ;; + adapters) run_local_adapters ;; + check-adapters) run_local_check_adapters ;; + build) run_local_build ;; + all) run_local_all ;; + clean) + rm -rf "$BUILD_DIR" + echo "Removed $BUILD_DIR" + ;; + help|-h|--help) usage ;; + *) + usage + exit 2 + ;; + esac +} + +# Run the task in the container, mapping file ownership so generated files stay +# owned by the invoking user on native Linux. Docker runs as root by default, so +# pass an explicit user plus a writable HOME. Rootless Podman maps container root +# to the host user with --userns=keep-id. +run_in_container() { + ENGINE="$1" + shift + + case "$ENGINE" in + podman) + podman run --rm \ + --userns=keep-id \ + -e DOCS_TOOLBOX_IN_CONTAINER=1 \ + -e BUILD_DIR="$BUILD_DIR" \ + -e SOURCE_DOC="$SOURCE_DOC" \ + -e HOME=/tmp \ + -v "$PWD":/app \ + -w /app \ + "$DOCS_TOOLBOX_IMAGE" \ + sh ./build.sh "$COMMAND" "$@" + ;; + docker) + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e DOCS_TOOLBOX_IN_CONTAINER=1 \ + -e BUILD_DIR="$BUILD_DIR" \ + -e SOURCE_DOC="$SOURCE_DOC" \ + -e HOME=/tmp \ + -v "$PWD":/app \ + -w /app \ + "$DOCS_TOOLBOX_IMAGE" \ + sh ./build.sh "$COMMAND" "$@" + ;; + esac +} + +# Inside the container: always run locally against the image toolchain. +if [ "${DOCS_TOOLBOX_IN_CONTAINER:-}" = "1" ]; then + run_local "$@" + exit 0 +fi + +case "$COMMAND" in + clean|help|-h|--help) + run_local "$@" + ;; + validate|generate|adapters|check-adapters|build|all) + if [ "${DOCS_TOOLBOX_LOCAL:-}" = "1" ]; then + echo "DOCS_TOOLBOX_LOCAL=1: running '$COMMAND' against the host toolchain (not reproducible)." >&2 + run_local "$@" + else + ENGINE="$(find_engine)" + if [ -n "$ENGINE" ]; then + run_in_container "$ENGINE" "$@" + else + echo "No running container engine (Docker or Podman) found." >&2 + echo "Start one to run '$COMMAND' in the reproducible docs-toolbox image," >&2 + echo "or set DOCS_TOOLBOX_LOCAL=1 to run against the host toolchain." >&2 + exit 1 + fi + fi + ;; + *) + usage + exit 2 + ;; +esac From f3f636661262ff987252ffceaf7e0c16309e7376 Mon Sep 17 00:00:00 2001 From: Dieter Baier Date: Sun, 12 Jul 2026 14:43:55 +0200 Subject: [PATCH 2/2] Test the docs-toolbox build.sh template (PR review) The generic templates/scripts/build.sh was only documented, not exercised. Add test/build-sh-template.test.mjs: assert `sh -n` validates the script, and that local-mode dispatch invokes the expected vendored tool for validate, generate, and check-adapters using stub ruby/node on PATH. Wire it into the test-js task (and README) so `./build.sh test` and CI run it. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 5 ++- build.sh | 2 +- test/build-sh-template.test.mjs | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 test/build-sh-template.test.mjs diff --git a/README.md b/README.md index c9d7308..ac252c4 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,8 @@ Local equivalent (without docs-toolbox): ruby -Itest test/validate_metamodel_test.rb # validator + generator units ruby -Itest test/validate_metamodel_cli_test.rb # validator CLI behaviour node --test test/build-agent-adapters.test.mjs \ - test/build-agent-adapters-template.test.mjs # adapter generator + template + test/build-agent-adapters-template.test.mjs \ + test/build-sh-template.test.mjs # adapter generator + build.sh templates ``` The container-based render scripts (`build.sh` itself and @@ -344,7 +345,7 @@ example to pin a digest) with `DOCS_TOOLBOX_IMAGE`. | `generate` | Validate, then generate derived fragments/indexes | `ruby scripts/validate-metamodel.rb --generate` | | `test` | Run all tests (Ruby units, Ruby CLI, JS adapter) | see [Tests](#tests) | | `test-ruby` | Ruby validator/generator unit and CLI tests | `ruby -Itest test/validate_metamodel_test.rb` and `ruby -Itest test/validate_metamodel_cli_test.rb` | -| `test-js` | JS adapter generator tests | `node --test test/build-agent-adapters.test.mjs test/build-agent-adapters-template.test.mjs` | +| `test-js` | JS adapter generator + build.sh template tests | `node --test test/build-agent-adapters.test.mjs test/build-agent-adapters-template.test.mjs test/build-sh-template.test.mjs` | | `adapters` | Regenerate agent adapters from skills | `node scripts/build-agent-adapters.js` | | `check-adapters` | Fail if the generated adapters are stale | `node scripts/check-agent-adapters.js` | | `build` | Generate fragments and render architecture HTML | see below | diff --git a/build.sh b/build.sh index df2f835..9b9181c 100755 --- a/build.sh +++ b/build.sh @@ -78,7 +78,7 @@ run_local_test_ruby() { } run_local_test_js() { - node --test test/build-agent-adapters.test.mjs test/build-agent-adapters-template.test.mjs + node --test test/build-agent-adapters.test.mjs test/build-agent-adapters-template.test.mjs test/build-sh-template.test.mjs } run_local_test() { diff --git a/test/build-sh-template.test.mjs b/test/build-sh-template.test.mjs new file mode 100644 index 0000000..40340a1 --- /dev/null +++ b/test/build-sh-template.test.mjs @@ -0,0 +1,76 @@ +// Behaviour specification for the generic docs-toolbox task runner template +// (templates/scripts/build.sh) that consuming projects copy to their root. +// +// The runner carries shell, container, and argument-forwarding logic but is only +// documented, not exercised, elsewhere. These tests check that it is at least +// syntactically valid and that its local-mode dispatch invokes the expected +// vendored tool for each task, using stub `ruby`/`node` on PATH. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const templateBuildSh = path.join(repoRoot, "templates", "scripts", "build.sh"); + +test("template build.sh is valid POSIX shell (sh -n)", () => { + const result = spawnSync("sh", ["-n", templateBuildSh], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); +}); + +// Set up a throwaway consuming project with build.sh at its root and stub +// `ruby`/`node` executables that record their arguments, then run a task in +// local mode and return what the stubs saw. +function runTaskWithStubs(t, task) { + const parent = fs.mkdtempSync(path.join(os.tmpdir(), "akt-buildsh-")); + t.after(() => fs.rmSync(parent, { recursive: true, force: true })); + + fs.copyFileSync(templateBuildSh, path.join(parent, "build.sh")); + fs.mkdirSync(path.join(parent, "scripts"), { recursive: true }); + + const binDir = path.join(parent, "stub-bin"); + fs.mkdirSync(binDir); + const log = path.join(parent, "invocations.log"); + for (const tool of ["ruby", "node", "asciidoctor"]) { + const stub = path.join(binDir, tool); + fs.writeFileSync(stub, `#!/bin/sh\necho "${tool} $*" >> "$STUB_LOG"\n`); + fs.chmodSync(stub, 0o755); + } + + const result = spawnSync("sh", ["build.sh", task], { + cwd: parent, + encoding: "utf8", + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH}`, + DOCS_TOOLBOX_LOCAL: "1", + // eslint-disable-next-line no-undef + STUB_LOG: log, + }, + }); + + const invocations = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : ""; + return { result, invocations }; +} + +test("local-mode validate runs the metamodel validator", (t) => { + const { result, invocations } = runTaskWithStubs(t, "validate"); + assert.equal(result.status, 0, result.stderr); + assert.match(invocations, /ruby scripts\/validate-metamodel\.rb/); +}); + +test("local-mode generate runs the validator with --generate", (t) => { + const { result, invocations } = runTaskWithStubs(t, "generate"); + assert.equal(result.status, 0, result.stderr); + assert.match(invocations, /ruby scripts\/validate-metamodel\.rb --generate/); +}); + +test("local-mode check-adapters runs the adapter checker", (t) => { + const { result, invocations } = runTaskWithStubs(t, "check-adapters"); + assert.equal(result.status, 0, result.stderr); + assert.match(invocations, /node scripts\/check-agent-adapters\.js/); +});