Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -279,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
Expand Down Expand Up @@ -332,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 |
Expand Down
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
7 changes: 7 additions & 0 deletions skills/bootstrap-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
201 changes: 201 additions & 0 deletions templates/scripts/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#!/usr/bin/env sh

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der neue Runner ist produktive Bootstrap-Logik, wird in diesem PR aber offenbar nur dokumentiert und nicht selbst getestet. Bitte mindestens sh -n templates/scripts/build.sh sowie einen kleinen Test des Local-Mode-Dispatchs (validate/generate/check-adapters, gern mit Stub-Kommandos in PATH) in CI aufnehmen. Noch besser wäre, das example/ mit einer wörtlichen Kopie des Templates auszustatten und dort check/build auszuführen. Sonst kann der Templatepfad unbemerkt brechen, während die toolkit-eigene build.sh weiterhin grün bleibt.

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 <task> [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
76 changes: 76 additions & 0 deletions test/build-sh-template.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
Loading