This post continues from the twenty-third part. An architecture model that is only validated locally is only half protected. As soon as multiple people work on the model, CI is needed: automatic validation on every push, a visible diff on every pull request.

Installing Bausteinsicht in the Pipeline

GitHub Actions does not provide a Bausteinsicht action — installation is done manually via curl:

- name: Install Bausteinsicht
  run: |
    curl -Lo bausteinsicht.tar.gz \
      https://github.com/docToolchain/Bausteinsicht/releases/latest/download/bausteinsicht_linux_amd64.tar.gz
    tar xzf bausteinsicht.tar.gz
    sudo mv bausteinsicht /usr/local/bin/
    bausteinsicht --version

For reproducible builds a fixed version is recommended instead of latest:

https://github.com/docToolchain/Bausteinsicht/releases/download/v0.5.0/bausteinsicht_linux_amd64.tar.gz

validate as a Build Gate

bausteinsicht validate returns exit code 1 when the model contains errors — GitHub Actions aborts the job:

name: Architecture Validation
on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Bausteinsicht
        run: |
          curl -Lo bausteinsicht.tar.gz \
            https://github.com/docToolchain/Bausteinsicht/releases/latest/download/bausteinsicht_linux_amd64.tar.gz
          tar xzf bausteinsicht.tar.gz && sudo mv bausteinsicht /usr/local/bin/

      - name: Validate architecture model
        run: bausteinsicht validate --format json
        working-directory: architecture/

With --format json the output lands as structured JSON on stdout — easier to parse when you want to further process the error list.

What validate checks (→ Part 8):

  • All element IDs in relationships and views exist in the model

  • All tag IDs used are defined in specification.tags

  • No element references an unknown type

  • Views do not have an empty include block

diff in Pull Requests

bausteinsicht diff compares two model states and shows which elements, relationships, and views have changed. In the PR context, HEAD~1 vs HEAD is the sensible comparison:

      - name: Compute model diff
        id: diff
        working-directory: architecture/
        run: |
          DIFF=$(bausteinsicht diff HEAD~1 HEAD --format json 2>/dev/null || echo '{}')
          echo "result=$DIFF" >> $GITHUB_OUTPUT

      - name: Comment diff on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const diff = JSON.parse('${{ steps.diff.outputs.result }}');
            if (!diff.changes || diff.changes.length === 0) return;
            const body = [
              '## 🏗 Architecture Changes',
              diff.changes.map(c => `- **${c.type}** \`${c.id}\`: ${c.description}`).join('\n')
            ].join('\n');
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body
            });
For the very first commit in a branch (HEAD~1 does not exist), diff returns an error — the || echo '{}' handles this case.

SVG Export After Every Push

Having architecture diagrams as SVG in the repository means: previews in GitHub, direct embedding in documentation, no local installation needed to view the diagrams.

      - name: Export architecture diagrams
        run: bausteinsicht export --format svg --output out/
        working-directory: architecture/

      - name: Upload SVG artifacts
        uses: actions/upload-artifact@v4
        with:
          name: architecture-diagrams
          path: architecture/out/*.svg
          retention-days: 30

Optionally: commit the exported SVGs directly back to the branch:

      - name: Commit exported SVGs
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add architecture/out/*.svg
          git diff --staged --quiet || git commit -m "chore: update architecture diagrams [skip ci]"
          git push

The [skip ci] prevents an infinite loop: the commit created by Actions does not trigger a new CI run.

Complete Workflow

name: Architecture CI
on:
  push:
    paths:
      - 'architecture/**'
  pull_request:
    paths:
      - 'architecture/**'

jobs:
  architecture:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # for diff HEAD~1

      - name: Install Bausteinsicht
        run: |
          curl -Lo bausteinsicht.tar.gz \
            https://github.com/docToolchain/Bausteinsicht/releases/latest/download/bausteinsicht_linux_amd64.tar.gz
          tar xzf bausteinsicht.tar.gz && sudo mv bausteinsicht /usr/local/bin/

      - name: Validate model
        run: bausteinsicht validate --format json
        working-directory: architecture/

      - name: Compute diff (PR only)
        if: github.event_name == 'pull_request'
        id: diff
        working-directory: architecture/
        run: |
          DIFF=$(bausteinsicht diff HEAD~1 HEAD --format json 2>/dev/null || echo '{}')
          echo "result=$DIFF" >> $GITHUB_OUTPUT

      - name: Comment diff on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const diff = JSON.parse('${{ steps.diff.outputs.result }}');
            if (!diff.changes || diff.changes.length === 0) return;
            const body = [
              '## 🏗 Architecture Changes',
              diff.changes.map(c => `- **${c.type}** \`${c.id}\`: ${c.description}`).join('\n')
            ].join('\n');
            github.rest.issues.createComment({
              owner: context.repo.owner, repo: context.repo.repo,
              issue_number: context.issue.number, body
            });

      - name: Export SVGs
        run: bausteinsicht export --format svg --output out/
        working-directory: architecture/

      - name: Commit SVGs to main
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add architecture/out/*.svg
          git diff --staged --quiet || git commit -m "chore: update architecture diagrams [skip ci]"
          git push
paths: ['architecture/**'] ensures the workflow only runs when something in the model has actually changed — not on every commit.

Example Model

The example for this part (model with developer and CI/CD actors) is located at teil_24.jsonc.

This is what the result looks like in draw.io (bausteinsicht sync):

The draw.io file for this can be found here: teil_24.drawio

Generated PNG files via bausteinsicht export --image-format png:

containers
context

Generated PlantUML diagrams via bausteinsicht export-diagram:

Diagram
Diagram

Up Next: Migration

CI protects the existing model. But what if you are coming from another tool? The next part covers how to migrate from draw.io, PlantUML, and Structurizr to Bausteinsicht — what can be done automatically and what needs to be transferred manually.

Official documentation: User Manual · Tutorial on doctoolchain.org