Reference

Sync System

How Markdown files in the Git repository are published to BookStack pages via the BookStack REST API — automatically or on demand.

How it works

The sync is handled by .github/scripts/bookstack-sync.py, a Python script that calls the BookStack REST API. For each Markdown file passed to it:

  1. 1

    Derive the page name

    The filename stem is converted to Title Case — hyphens and underscores become spaces. E.g. old-fashioned.mdOld Fashioned.

  2. 2

    Search BookStack for an existing page

    The API is queried for a page with that exact name (case-insensitive).

  3. 3a

    Update — if the page exists

    The page content is replaced via PUT /api/pages/{id}. Images are uploaded first; local paths are substituted with BookStack URLs before the update.

  4. 3b

    Create — if the page does not exist

    A new page is created in the target book via POST /api/pages. Images are then uploaded and the page is updated with resolved URLs.

A preflight check runs before any sync: the script attempts a GET /api/search to verify connectivity and token validity. If this fails, the sync aborts immediately with a descriptive error.

Directory Mapping (bookstack-config.json)

By default, every file lands in the book identified by BOOKSTACK_BOOK_ID. To route files in different directories to different books, create a bookstack-config.json at the repo root:

bookstack-config.json
{
    "directory_map": {
        "cocktails": "env:BOOKSTACK_COCKTAILS_BOOK_ID",
        "whiskeys":  "env:BOOKSTACK_WHISKEY_BOOK_ID"
    },
    "default_book_id": "env:BOOKSTACK_BOOK_ID"
}

The "env:VAR_NAME" syntax reads the book ID from an environment variable at runtime, keeping IDs out of the repository. You can also use plain integers:

bookstack-config.json (inline IDs)
{
    "directory_map": {
        "cocktails": 3,
        "whiskeys":  7
    },
    "default_book_id": 1
}

Lookup behaviour

The script walks up the path of each file looking for a matching directory name in directory_map. For example, cocktails/negroni.md matches the "cocktails" key. If no match is found, default_book_id (or BOOKSTACK_BOOK_ID) is used. If no fallback is configured, the sync raises an error.

Environment Variables

VariableRequiredDescription
BOOKSTACK_URL Required Base URL of the BookStack instance — no trailing slash, no /api suffix. E.g. http://bookstack-bookstack-dev.apps-crc.testing
BOOKSTACK_TOKEN_ID Required API token ID. Generated in BookStack → My Account → API Tokens.
BOOKSTACK_TOKEN_SECRET Required API token secret (shown once at creation time).
BOOKSTACK_BOOK_ID Optional* Default book ID for new pages. Optional if bookstack-config.json covers all directories.
BOOKSTACK_COCKTAILS_BOOK_ID Optional Book ID for files under cocktails/ (used when config references env:BOOKSTACK_COCKTAILS_BOOK_ID).
BOOKSTACK_WHISKEY_BOOK_ID Optional Book ID for files under whiskeys/.

Local Sync (sync.sh)

For running the sync from your workstation — without going through CI — use sync.sh. It reads credentials from a .sync-env file (git-ignored) and auto-resolves the BookStack URL from the CRC route if BOOKSTACK_URL is not set.

First-time setup

shell
# Copy the example credentials file
cp .sync-env.example .sync-env

# Fill in your token values
$EDITOR .sync-env
.sync-env
export BOOKSTACK_URL=""                  # leave blank to auto-resolve from CRC route
export BOOKSTACK_TOKEN_ID="your-token-id"
export BOOKSTACK_TOKEN_SECRET="your-token-secret"
export BOOKSTACK_BOOK_ID="your-book-id"
⚠️ Never commit .sync-env The file is listed in .gitignore. Keep it out of version control — it contains live API credentials.

Usage

CommandWhat it does
./sync.shSyncs only files changed vs origin/main
./sync.sh --allSyncs every .md file in the repo (excluding .github/)
./sync.sh cocktails/negroni.mdSyncs one or more specific files

The script creates a Python virtualenv in .venv/ on first run and installs requests automatically.

Image Handling

BookStack pages often include images. The sync script handles the upload automatically:

💡 Image naming convention Images should be placed at <content-dir>/images/<slug>.jpg where the slug matches the Markdown filename. E.g. for cocktails/negroni.md, the image lives at cocktails/images/negroni.jpg.

Rate Limiting & Retries

The BookStack API enforces rate limits. The sync script handles this gracefully:

Rate-limit behaviour summary
Attempt 1: wait 2s
Attempt 2: wait 4s
Attempt 3: wait 8s
Attempt 4: wait 16s
Attempt 5: raise_for_status()

Plan Mode (--plan)

Pass --plan as the first argument to preview what the sync would do without making any changes. This is used in the CD pipeline to produce a summary in the GitHub Actions step summary.

shell
python .github/scripts/bookstack-sync.py --plan cocktails/negroni.md whiskeys/ardbeg-10.md

Output includes a Terraform-style plan and a Mermaid diagram:

BookStack will perform the following actions:

  # page "Negroni" will be created
  + page {
      + name    = "Negroni"
      + book_id = 3
    }

  # page "Ardbeg 10" will be updated in-place
  ~ page {
      ~ name    = "Ardbeg 10"
      ~ markdown = (content updated)
    }

Plan: 1 to create, 1 to update.

Troubleshooting

ErrorCauseFix
Could not connect to BookStack BOOKSTACK_URL is wrong or the instance is not running Verify the URL and that the BookStack pod is Ready
Authentication failed (401) Invalid BOOKSTACK_TOKEN_ID or BOOKSTACK_TOKEN_SECRET Regenerate the API token in BookStack → My Account → API Tokens
Permission denied (403) API token exists but lacks permission to read/write pages Ensure the token's user has at least Editor role in BookStack
API not found (404) URL contains a path suffix, or BookStack API is disabled Use the base URL only — no /api or /index.php
No book ID configured for '…' File is in an unmapped directory and BOOKSTACK_BOOK_ID is not set Add the directory to bookstack-config.json or set BOOKSTACK_BOOK_ID
Image uploaded but URL not substituted Image path in Markdown uses an absolute URL or wrong relative path Ensure images use relative paths like images/slug.jpg