Books from other GitHub repositories
A book in your collection does not have to live in your own folder: you can include a book from another GitHub repository, kept by its author in their own repository. The author has nothing to do: no workflow, token or BookMD installation is needed, only the Markdown files in the repository. The download and the build run in the workflow of your collection.
This guide assumes that you already have a working collection (see publishing a collection), for example from the template repository.
1. The author's part: the entry file
The front page of an included book is a Markdown file in the repository (for example book.md), and its folder is the content root of the book. The file may have an id: a suggestion for the address of the book in a collection. The collection decides the address, see section 2; without an id the address comes from the folder of the file.
---
id: web-programming
title: Web programming
language: en
children:
- "[[01-intro.md]]"
---
# Web programming
- The address (an
id or a slug) may contain only lower-case letters, digits and single hyphens (it cannot start or end with a hyphen, and cannot contain two hyphens in a row), at most 64 characters.
_app, 404, @dev, content-assets, assets, static and favicon.ico are reserved.
- The address must not collide with that of another included book, nor with the name of a file or folder in your
content/ folder.
- The other metadata (
name, language, author, tags, children) is the same as for local books, see the authoring guide.
- The files of the book can refer to anything in their own repository, for example to a shared
_common/ folder next to the book's folder (sources: ["../../_common/part.md"]). A link to another repository, to another book of your collection or to your content/ folder does not work. The book is still served at its id, whatever folder it is in.
2. Your part: one line in books.md
In the books list of content/books.md, next to the local [[…]] references, add a line in this form:
<ref>@github.com/<owner>/<repository>/<path-to-the-entry-file.md>
---
books:
- "[[my-first-book/book.md]]"
- "main@github.com/colleague/web-notes/book.md"
- "v2.1@github.com/other-colleague/algorithms/materials/book.md"
---
- The ref is required: a branch name (
main), a tag (v2.1) or a full commit SHA. There is no default branch. If a name is both a branch and a tag, the build reports an error; use a SHA then.
- The only host for now is
github.com.
- The path of the entry file is relative to the repository root and must end in
.md.
- The quotes are required.
Commit and push. On the next run the workflow downloads the book, and it appears in your collection (for example at /web-programming/).
The address of the book
The address belongs to your collection, not to the author, so you can choose it. Write the item as an object with a source and a slug:
---
books:
- "main@github.com/colleague/web-notes/book.md"
- source: "v2.1@github.com/other-colleague/algorithms/materials/book.md"
slug: algorithms
---
The address of a book is, in this order:
- the
slug in your books list,
- the
id in the entry file of the book, if it has one,
- the name of the folder of the entry file (
programming-5-hu/book.md gives programming-5-hu), or the name of the repository when the file is in the root.
A name is made of lower-case letters, digits and single hyphens (accents are removed, other characters become hyphens). If two books want the same address, or the address collides with a folder of your content/, the build tells you to set a slug. Changing the slug changes the address of the book, so choose it once: the author cannot change it by editing the book.
How does an included book update?
On every run the build downloads one specific commit, after resolving the ref. Therefore:
- Pinned version: if you give a SHA or a tag, the book does not change until you rewrite the line.
- Followed branch: if you give
main, the site shows the latest state of the book, but it only updates when your workflow runs. A push by the author does not start your workflow.
To follow a branch automatically, schedule the workflow. Add to the on: block, next to push:
on:
push:
branches: [main]
schedule:
- cron: '17 4 * * *'
workflow_dispatch:
You can also start it by hand with Actions → Deploy book → Run workflow.
Private repositories
A public book needs none of this. A private repository can be included too, but the build has to prove that it may read it. The built-in token of a GitHub Actions run (GITHUB_TOKEN) only works for the repository the workflow runs in, even inside the same organisation, so the collection uses a read-only GitHub App. Two people are involved:
- The operator (host) runs the collection. They create the App once, store its credentials as secrets in the collection, and list the book in
books.md.
- The author owns the private repository. They only install the operator's App on that repository and approve the publication. No token, secret or workflow is needed on their side. If you are both, you do both parts.
A private repository does not make the finished site private. That is why the author must approve the publication in the entry file (publish: true, step A2 below).
The operator: set up once
1. Create the App. For an organisation, open https://github.com/organizations/<organisation>/settings/apps/new (for a personal account: Settings → Developer settings → GitHub Apps → New GitHub App) and fill in:
- GitHub App name: for example
<organisation>-bookmd. Homepage URL: your site address.
- Webhook: untick Active. The App needs no webhook.
- Repository permissions → Contents: Read-only. (Metadata: Read-only is added automatically.) Give it nothing else.
- Where can this app be installed? Choose Only on this account when every book is in your own account or organisation. Choose Any account when authors from other accounts have to install it.
Click Create GitHub App. Use one App for one operator: its private key can read every repository the App is installed on, so do not share a key between unrelated sites.
2. Note the App ID and create a key. On the App's page copy the App ID (a number). In Private keys click Generate a private key: a .pem file is downloaded once. GitHub does not keep it, so you cannot download it again, only generate another one. An App can have several keys, so a key can be replaced without downtime.
3. Store the credentials as secrets in the repository of the collection. In the browser: Settings → Secrets and variables → Actions → New repository secret, twice:
| Secret |
Value |
BOOKMD_APP_ID |
the App ID |
BOOKMD_APP_PRIVATE_KEY |
the whole content of the .pem file, from -----BEGIN to -----END |
With the GitHub CLI, and without leaving the key on your disk:
gh secret set BOOKMD_APP_ID --repo <owner>/<collection> --body 123456
gh secret set BOOKMD_APP_PRIVATE_KEY --repo <owner>/<collection> < key.pem
rm key.pem
If you run several collections, create the two secrets once as organization secrets (Organization settings → Secrets and variables → Actions) and select the repositories that may use them.
4. Give the secrets to the build. A secret reaches a step only when the workflow passes it. The workflow of the template repository and the one in the publishing guide already do; if your workflow does not, add the two env lines to the steps that download books, check and build:
- run: bun run check
env:
BOOKMD_APP_ID: ${{ secrets.BOOKMD_APP_ID }}
BOOKMD_APP_PRIVATE_KEY: ${{ secrets.BOOKMD_APP_PRIVATE_KEY }}
- run: bun run build
env:
BASE_PATH: /${{ github.event.repository.name }}
BOOKMD_APP_ID: ${{ secrets.BOOKMD_APP_ID }}
BOOKMD_APP_PRIVATE_KEY: ${{ secrets.BOOKMD_APP_PRIVATE_KEY }}
If you use the plan workflow with separate steps (see the CLI), give the secrets only to bookmd plan and bookmd sources; check and build work from the downloaded commits without them.
5. Send the author the install address of your App: https://github.com/apps/<app-name>/installations/new (the name is in the address of the App's page).
The author: for each private book
A1. Install the App on the repository of the book: open the address from the operator, choose your account or organisation, select Only select repositories, pick the repository of the book and click Install. (You need to be an owner of the account, or an admin of the repository.) This gives read access to that repository only.
A2. Approve the publication in the entry file of the book, and give it an id:
---
id: web-programming
title: Web programming
language: en
publish: true
children:
- "[[01-intro.md]]"
---
Without publish: true the build skips the book, removes it from the catalog and prints a warning; the rest of the collection builds as usual, so a book that is still being written can already be listed.
A3. Tell the operator the address of the entry file: main@github.com/<owner>/<repository>/<path>/book.md.
To withdraw the access: Settings → Applications → Installed GitHub Apps → Configure and remove the repository. This stops further reads; it does not take back what has already been published.
The operator: add the book and check
Add the line from A3 to content/books.md (see above), commit and push. The run log ends the check with Generated N pages in M courses (K from Git); the number K counts the included books. If a book is missing or the run fails, see the list below.
How it works
The engine first tries to download every source anonymously, so a public source never needs a key. Only if that fails does it sign a short-lived App token, look up the installation of the repository and ask for a one-repository, read-only token. The token reaches Git through the environment, not through arguments or the URL, and is not written anywhere. The list of installations of the App is the access register.
In a public repository the logs and artifacts are public: they can reveal the names of private repositories.
Common errors
- "course id … is already used" / collision with local content: two books have the same address, or a folder under
content/ has the same name. Give one of them a different slug.
- Invalid
id or slug: the value does not follow the rules above (or a name cannot be derived from the folder, for example 404; set a slug). The book is left out (or replaced by its last good version) with a warning, see a book that does not build.
- Unknown ref: a mistyped branch or tag name; check that it exists in the repository.
- The repository is not readable (
cannot read repository …): a mistyped owner or repository name, or the repository is private and the App is not installed on it (operator step 5 / author step A1).
configure the BookMD GitHub App: the repository is private and the build has no credentials. The two secrets are missing, or the workflow does not pass them to the step (operator steps 3 and 4).
- A private book does not appear and the log warns about
publish: true: the entry file lacks publish: true (author step A2).
- A link in the book does not work: the linked file is outside the repository of the book; an included book is sealed and can only refer to files of its own repository.
Books from other GitHub repositories
A book in your collection does not have to live in your own folder: you can include a book from another GitHub repository, kept by its author in their own repository. The author has nothing to do: no workflow, token or BookMD installation is needed, only the Markdown files in the repository. The download and the build run in the workflow of your collection.
This guide assumes that you already have a working collection (see publishing a collection), for example from the template repository.
1. The author's part: the entry file
The front page of an included book is a Markdown file in the repository (for example
book.md), and its folder is the content root of the book. The file may have anid: a suggestion for the address of the book in a collection. The collection decides the address, see section 2; without anidthe address comes from the folder of the file.--- id: web-programming title: Web programming language: en children: - "[[01-intro.md]]" --- # Web programmingidor a slug) may contain only lower-case letters, digits and single hyphens (it cannot start or end with a hyphen, and cannot contain two hyphens in a row), at most 64 characters._app,404,@dev,content-assets,assets,staticandfavicon.icoare reserved.content/folder.name,language,author,tags,children) is the same as for local books, see the authoring guide._common/folder next to the book's folder (sources: ["../../_common/part.md"]). A link to another repository, to another book of your collection or to yourcontent/folder does not work. The book is still served at itsid, whatever folder it is in.2. Your part: one line in
books.mdIn the
bookslist ofcontent/books.md, next to the local[[…]]references, add a line in this form:--- books: - "[[my-first-book/book.md]]" - "main@github.com/colleague/web-notes/book.md" - "v2.1@github.com/other-colleague/algorithms/materials/book.md" ---main), a tag (v2.1) or a full commit SHA. There is no default branch. If a name is both a branch and a tag, the build reports an error; use a SHA then.github.com..md.Commit and push. On the next run the workflow downloads the book, and it appears in your collection (for example at
/web-programming/).The address of the book
The address belongs to your collection, not to the author, so you can choose it. Write the item as an object with a
sourceand aslug:--- books: - "main@github.com/colleague/web-notes/book.md" - source: "v2.1@github.com/other-colleague/algorithms/materials/book.md" slug: algorithms ---The address of a book is, in this order:
slugin yourbookslist,idin the entry file of the book, if it has one,programming-5-hu/book.mdgivesprogramming-5-hu), or the name of the repository when the file is in the root.A name is made of lower-case letters, digits and single hyphens (accents are removed, other characters become hyphens). If two books want the same address, or the address collides with a folder of your
content/, the build tells you to set aslug. Changing theslugchanges the address of the book, so choose it once: the author cannot change it by editing the book.How does an included book update?
On every run the build downloads one specific commit, after resolving the ref. Therefore:
main, the site shows the latest state of the book, but it only updates when your workflow runs. A push by the author does not start your workflow.To follow a branch automatically, schedule the workflow. Add to the
on:block, next topush:on: push: branches: [main] schedule: - cron: '17 4 * * *' # once a day workflow_dispatch:You can also start it by hand with Actions → Deploy book → Run workflow.
Private repositories
A public book needs none of this. A private repository can be included too, but the build has to prove that it may read it. The built-in token of a GitHub Actions run (
GITHUB_TOKEN) only works for the repository the workflow runs in, even inside the same organisation, so the collection uses a read-only GitHub App. Two people are involved:books.md.A private repository does not make the finished site private. That is why the author must approve the publication in the entry file (
publish: true, step A2 below).The operator: set up once
1. Create the App. For an organisation, open
https://github.com/organizations/<organisation>/settings/apps/new(for a personal account: Settings → Developer settings → GitHub Apps → New GitHub App) and fill in:<organisation>-bookmd. Homepage URL: your site address.Click Create GitHub App. Use one App for one operator: its private key can read every repository the App is installed on, so do not share a key between unrelated sites.
2. Note the App ID and create a key. On the App's page copy the App ID (a number). In Private keys click Generate a private key: a
.pemfile is downloaded once. GitHub does not keep it, so you cannot download it again, only generate another one. An App can have several keys, so a key can be replaced without downtime.3. Store the credentials as secrets in the repository of the collection. In the browser: Settings → Secrets and variables → Actions → New repository secret, twice:
BOOKMD_APP_IDBOOKMD_APP_PRIVATE_KEY.pemfile, from-----BEGINto-----ENDWith the GitHub CLI, and without leaving the key on your disk:
If you run several collections, create the two secrets once as organization secrets (Organization settings → Secrets and variables → Actions) and select the repositories that may use them.
4. Give the secrets to the build. A secret reaches a step only when the workflow passes it. The workflow of the template repository and the one in the publishing guide already do; if your workflow does not, add the two
envlines to the steps that download books,checkandbuild:- run: bun run check env: BOOKMD_APP_ID: ${{ secrets.BOOKMD_APP_ID }} BOOKMD_APP_PRIVATE_KEY: ${{ secrets.BOOKMD_APP_PRIVATE_KEY }} - run: bun run build env: BASE_PATH: /${{ github.event.repository.name }} BOOKMD_APP_ID: ${{ secrets.BOOKMD_APP_ID }} BOOKMD_APP_PRIVATE_KEY: ${{ secrets.BOOKMD_APP_PRIVATE_KEY }}If you use the plan workflow with separate steps (see the CLI), give the secrets only to
bookmd planandbookmd sources;checkandbuildwork from the downloaded commits without them.5. Send the author the install address of your App:
https://github.com/apps/<app-name>/installations/new(the name is in the address of the App's page).The author: for each private book
A1. Install the App on the repository of the book: open the address from the operator, choose your account or organisation, select Only select repositories, pick the repository of the book and click Install. (You need to be an owner of the account, or an admin of the repository.) This gives read access to that repository only.
A2. Approve the publication in the entry file of the book, and give it an
id:--- id: web-programming title: Web programming language: en publish: true children: - "[[01-intro.md]]" ---Without
publish: truethe build skips the book, removes it from the catalog and prints a warning; the rest of the collection builds as usual, so a book that is still being written can already be listed.A3. Tell the operator the address of the entry file:
main@github.com/<owner>/<repository>/<path>/book.md.To withdraw the access: Settings → Applications → Installed GitHub Apps → Configure and remove the repository. This stops further reads; it does not take back what has already been published.
The operator: add the book and check
Add the line from A3 to
content/books.md(see above), commit and push. The run log ends the check withGenerated N pages in M courses (K from Git); the number K counts the included books. If a book is missing or the run fails, see the list below.How it works
The engine first tries to download every source anonymously, so a public source never needs a key. Only if that fails does it sign a short-lived App token, look up the installation of the repository and ask for a one-repository, read-only token. The token reaches Git through the environment, not through arguments or the URL, and is not written anywhere. The list of installations of the App is the access register.
In a public repository the logs and artifacts are public: they can reveal the names of private repositories.
Common errors
content/has the same name. Give one of them a differentslug.idorslug: the value does not follow the rules above (or a name cannot be derived from the folder, for example404; set aslug). The book is left out (or replaced by its last good version) with a warning, see a book that does not build.cannot read repository …): a mistyped owner or repository name, or the repository is private and the App is not installed on it (operator step 5 / author step A1).configure the BookMD GitHub App: the repository is private and the build has no credentials. The two secrets are missing, or the workflow does not pass them to the step (operator steps 3 and 4).publish: true: the entry file lackspublish: true(author step A2).