Skip to content

Repo Blueprint

Documentation CI Variants CodeQL OpenSSF Scorecard OpenSSF Best Practices REUSE License: MIT-0 Sponsor

Repo Blueprint for GitHub repositories: seven kinds of projects with one standard, and a terminal in which copier copy asks for the kind of project, the checks and bots arrive, blueprint.py settings apply sets the rulesets, environments and labels, and copier update follows every new release

A Copier template for GitHub repositories that look after themselves: tests on every change, a release bot, security checks, an issue assistant and the settings of the repository as code, for seven kinds of projects. A bot brings every repository made from it up to each new release of the blueprint.

The website of the blueprint has this guide in English and German, together with the roadmap, the security design, the decisions and the dashboard.

💛 If the blueprint is useful to you, you can support its development. Deutsche Fassung.

How a repository looks after itself

Every change, yours or a bot's, is a pull request that merges once every check passes, and the release bot makes a signed release of what reaches main; three bots keep the repository current on their own. In the pictures, blue is the blueprint and its bots, orange is you and what is yours, and green is what comes out.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How a repository looks after itself
    accDescr: copier copy makes a repository from the blueprint. Every change, yours or a bot's, is a pull request that merges once every check passes, and the release bot makes a signed release of what reaches main. Renovate, the blueprint bot and the settings bot keep the repository current on their own.

    blueprint[("Repo Blueprint")]
    you(["You"])
    renovate(["Renovate<br>every 2 hours"])
    updater(["Blueprint bot<br>after every release<br>and every week"])
    settings(["Settings bot<br>every week"])

    subgraph repo ["Your repository"]
        pr["Pull request"] --> checks{"Every check<br>passes?"}
        checks -- "yes" --> main["main"]
        main -- "release bot" --> release["Signed release"]
    end

    blueprint -- "copier copy" --> repo
    blueprint -. "a new release" .-> updater
    you -- "a change" --> pr
    renovate -- "updates" --> pr
    updater -- "copier update" --> pr
    settings -- "the settings as code" --> repo

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class renovate,updater,settings bot
    class you person
    class release done

What a new repository gets

Area What runs How
Checks scripts/check.sh on every change, the same locally and in the CI: tests with full coverage, on every core in Python projects, types, lint, packaging; a passing error of a download fails no run, because every download of the workflows tries again; the CI installs the Python tools with uv in seconds and keeps the packages of Composer and npm, krankerl and what mypy knows of the code in its cache, php-cs-fixer checks only the files that changed, Psalm reads the code of a Nextcloud app and what it uses, not all of vendor/, and a step that installs, builds or downloads ends after five minutes instead of holding a pull request the tools of each kind of project, hash-pinned
Lint the workflows (actionlint), the license of every file (REUSE), the Markdown of every document (markdownlint), hints at the spelling in English and German (cspell) and at the style of the English (Vale) of the changed documents, the sign-off of every commit (DCO), the title of every pull request (Conventional Commits), an entry in the changelog for every change for users, a fragment in changelog.d/ or a line under Unreleased, checked as the release takes it in lint.yml, pull-request-title.yml
Security CodeQL, OpenSSF Scorecard, dependency review, secret scan, SBOMs as SPDX and CycloneDX, OpenVEX, release tags signed without a key (gitsign) and checked before every build, findings watcher, the security audit of the workflows (zizmor), every lock file against the OSV database (OSV-Scanner), the network traffic of every job (Harden-Runner) every action pinned to a commit hash
Releases a pull request with the next version, decided from the titles of the merged pull requests (fix a patch, feat a minor, ! a major version, or a minor one while the version is below 1.0.0), and the text of the fragments in changelog.d/ and of Unreleased in the changelog as its notes; merging it publishes the release with its package, SBOM and the signed provenance of an isolated build (SLSA Build Level 3), delivers it, and then verifies it as its users can, also every week; where the repository variable BETA_CHANNEL is true, every change for users also becomes a beta of the next release, a prerelease for testers release-please, the release app, immutable releases, verify-release.yml
Dependencies Renovate, which the blueprint runs for every repository every two hours as the release app: the actions, the dependencies, the images and the pins of the workflows, a week after their release, and at once an update that fixes a vulnerability or the new commit of a branch that an action follows; routine updates and releases of dependency updates merge themselves once every check passes, while the weekly refresh of every lock file, which brings the newest versions without the week of waiting, waits for the maintainer; the blueprint checks that Renovate reads every lock file that it brings. Dependabot keeps the Features of the dev container. renovate.json5, renovate-blueprint.json5, dependabot.yml
Coverage one comment on every pull request with the coverage of its tests against that of main, file by file after-checks.yml
PR coach every check that fails says what it found, why that matters and how to fix it, with the commands to run: in the summary of the check, as its annotation and in one comment on the pull request that follows every push, also on a pull request from a fork; a title that is no Conventional Commit gets one that passes, and scripts/check.sh explains the step that stopped it on your machine too .github/checks.toml, .github/checks.project.toml, after-checks.yml
Fix bot a check that fails fixes what a command can fix, such as the format of the code, trailing whitespace and line endings, the rules of markdownlint and the pages in one column: the bot commits the fixes onto a branch of the repository, also one of Renovate, as one commit, suggests them at their lines on a pull request from a fork, and keeps one pull request with the fixes of main that merges itself; python3 .github/blueprint.py fix run makes the same fixes on your machine autofix in .github/checks.toml, after-checks.yml
Mutation tests every week, thousands of small changes of the code run against the tests, and the summary shows the share they notice and the mutants that survive; a low score is a report, never a failure, but a run that measures nothing fails; a pull request that changes how they run tries them mutation.yml: mutmut, Infection
Flaky tests a test run that fails, or whose job runs out of time, runs its failed jobs once more, unless the same jobs broke for the commit before; a job that passes then is reported as flaky in one issue, with the step, the test and the error that failed after-checks.yml
Cancelled checks the quick checks run once for each commit, so that no run for another commit cancels them; a run of a longer required check for the latest commit of a pull request that a late run for an older commit cancelled starts again by itself instead of blocking the merge after-checks.yml
Failed releases the release of a merge that fails, as at a push of its tag that GitHub refuses for a moment, runs its failed jobs once more by itself, where they stopped after-checks.yml
Bot health a bot or a scheduled workflow that fails on main runs its failed jobs once more where a second try can pass, and otherwise keeps one issue per workflow open, with the failed jobs, their steps and their first errors, which the next run that passes closes; every night, a look at the latest run of every workflow, those of the project too, which starts a workflow whose schedule GitHub missed or switched off; in the blueprint, one issue with the warnings and errors of Renovate bot-health.yml, blueprint.py bot-health
Fast checks a check runs only where it can change a result: a scanner in the pull requests that change what it reads, on main only what keeps a baseline or publishes, the rest every week, and in the release pull request no scanner at all; the newest Python checks the main of a Python project after every change and every week the triggers and path filters of the workflows, newest-python.yml
Issues a first analysis of every new issue by an AI that only reads, labels, duplicates, reminders, and closing with the release that ships the fix the issue assistant
Community README, contributing guide with the coding standards and the rule that every change brings its tests, code of conduct, security policy with how to verify a release and an assurance case, the security design of the software (docs/security.md), support, governance with the roles and the continuity of the project, issue forms, pull request template, records of the decisions that shape the project (docs/decisions/), discussions with forms for questions and ideas and an announcement of every release, sponsor button, social preview
Website the documentation in docs/ as a website with search and a light and a dark theme, in English and, with German pages such as index.de.md next to the English ones, in German: every pull request builds it strictly, and so does scripts/check.sh once the tools of its preview are installed, on Linux, macOS and Windows; every change of main publishes the build of that check on GitHub Pages, with what a hook writes into site/.well-known/, with the fonts and scripts that it downloads kept in a cache for a week MkDocs with the Material theme, mkdocs.yml, docs.yml
Diagrams every diagram as a picture that every reader sees, also in the GitHub app, the stores and e-mails, which draw no Mermaid: a Mermaid source in docs/diagrams/, drawn as an SVG for the light and one for the dark theme, while the website draws the source live; the check diagrams draws the pictures anew in every pull request that changes a diagram or adds a mermaid block, and the diagram bot commits the sources and pictures onto its branch; on main, a pull request of the diagram bot fixes what is still missing and merges itself; bash scripts/diagrams.sh --extract turns the mermaid blocks of existing Markdown into sources and their pictures scripts/diagrams.sh with mermaid-cli, lint.yml, after-checks.yml, diagrams.yml
Settings the settings of the repository as code: merges, rulesets, security, Actions, environments, variables, labels, GitHub Pages, and every file of the community standards of GitHub; the settings bot applies them after every change and every week .github/repository.toml and blueprint.py
Clean-up every week, the branches that main holds and the caches of closed pull requests go; a branch with work of its own stays and is listed after a month cleanup.yml
Development a dev container for VS Code and GitHub Codespaces with the tools of the checks and of the website, and a hook that runs them before every push, on Linux, macOS and Windows: scripts/check.sh fails when Git would skip it .devcontainer/, .githooks/pre-push
Coding agents the rules of the repository for Claude Code, Codex, GitHub Copilot and Cursor: the commands of the checks and tests of its kind, the titles of pull requests and the versions they make, the changelog, the sign-off, and which files belong to the project and which to the blueprint, a list that a test of the blueprint keeps equal to the files an update never overwrites; the notes of the project join them from a file of its own AGENTS.md, CLAUDE.md, .github/agents.project.md
Updates right after every release of the blueprint, and every week, the blueprint bot runs copier update and opens a pull request blueprint-update.yml
App Store for a Nextcloud app: the registration of its id with its certificate, once, by hand register-app.yml
Upstream for a Nextcloud app, every week: when Nextcloud has a new major version, a pull request raises max-version and merges itself once every check, the end-to-end tests included, passes against it; started by hand as a dry run, it shows the pull request it would open and changes nothing upstream.yml
Area labels every pull request gets the labels of the areas whose files it changes, by the paths in .github/labeler.yml, which belongs to the project area-labels.yml
Branches after every change of main, the branch bot brings each pull request that waits for auto-merge up to date, Renovate's too, so that it merges once its checks pass update-branches.yml

From a change to a release

The title of a pull request decides the next version, and merging the release pull request does the rest:

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: From a change to a release
    accDescr: A pull request whose title is fix, feat or feat! merges into main once every check passes. The release bot keeps a release pull request with the next version and the text of the fragments and of Unreleased. Merging it signs its tag first, in the next run of the release bot, then builds the release in isolation once the tag checks out, signs and publishes it, delivers it and verifies it, then and every week. With BETA_CHANNEL, every change for users also becomes a beta.

    pr["Pull request<br>fix: · feat: · feat!:"]
    main["main"]
    rpr["`Release pull request<br>chore: release x.y.z<br>notes from changelog.d/<br>and *Unreleased*`"]
    beta["Beta x.y.z-beta.N<br>for testers"]
    tag["Signed tag<br>at the merge, by the next run<br>checked before the build"]
    build["Isolated build<br>SLSA Build Level 3"]
    release["Release<br>package · SBOMs<br>provenance"]
    deliver["Delivery<br>PyPI · npm · GHCR<br>App Store · HACS"]
    verify["Verified as users can<br>at once and every week"]

    pr -- "every check passes" --> main
    main -- "release bot" --> rpr
    main -. "BETA_CHANNEL" .-> beta
    rpr -- "merged" --> tag --> build --> release
    release --> deliver
    release --> verify

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class rpr bot
    class release done

A pull request describes what it changes for users in a file of its own, a fragment in changelog.d/, such as changelog.d/dark-mode.md, below the heading of its kind of change, such as ### Features. A new file conflicts with nothing, so pull requests that run at the same time merge one after another, without a person who merges main into each of them, and a release no longer conflicts with the pull requests that wait. The check changelog checks each fragment of a pull request as the release takes it in and counts only a new one as its entry, scripts/check.sh checks every fragment that waits on main as well, and the release bot moves every fragment into the section of the release and deletes it in the same commit. A line under ## Unreleased in CHANGELOG.md still works.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: The changelog without conflicts
    accDescr: Two pull requests each add a file of their own to changelog.d, so they merge into main one after the other without a conflict. The release bot writes the section of the next release from the fragments and the text of Unreleased into CHANGELOG.md of the release pull request, and deletes the fragments in the same commit. Merging it makes that section the notes of the release.

    a["Pull request<br>changelog.d/dark-mode.md"]
    b["Pull request<br>changelog.d/token-secret.md"]
    main["main<br>one file for each change"]
    rpr["Release pull request<br>the section of x.y.z in CHANGELOG.md<br>the fragments deleted"]
    notes["Release notes<br>each kind of change below one heading"]

    a -- "merged" --> main
    b -- "merged, without a conflict" --> main
    main -- "release bot" --> rpr
    rpr -- "merged" --> notes

    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class a,b person
    class rpr bot
    class notes done

When the checks run

A check runs only where it can change a result, because an account of GitHub runs about 20 jobs at once, and each job that can't fail keeps a pull request waiting for a runner:

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: When the checks run
    accDescr: Every pull request runs the required checks, and the scanners of what it changes: zizmor for a workflow, OSV-Scanner for a lock file, fuzzing for Python code and the link check for a document. The release pull request, which changes only the changelog and the version, runs the required checks without the scanners. After a merge, main runs only what keeps a baseline or publishes. Every week, main runs the scanners in full, the secret scan of the whole history, the SBOM and the newest Python.

    pr["Pull request"]
    rpr["Release pull request<br>changelog and version"]
    required["Required checks<br>tests · lint · docs · CodeQL<br>dependency review · secret scan"]
    scanners["Scanners of what it changes<br>workflows: zizmor · lock files: OSV-Scanner<br>Python: fuzzing · documents: links"]
    main["main"]
    baseline["What keeps a baseline or publishes<br>tests with coverage · newest Python<br>CodeQL · zizmor · OSV-Scanner · Scorecard<br>website · release · branch bot"]
    weekly["Scanners in full · fuzzing for 15 minutes<br>secret scan of the whole history<br>SBOM · newest Python"]

    pr --> required
    pr -- "changes what they read" --> scanners
    rpr -- "no scanner" --> required
    required -- "every check passes" --> main
    main -- "after a merge" --> baseline
    main -- "every week" --> weekly

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class pr person
    class rpr bot
    class main done
  • After a merge: main takes a pull request only when it is up to date with it, so the merge gives main the files that the checks of the pull request passed. main runs only the tests, whose coverage the coverage bot compares every pull request with, the analyses that code scanning compares with, and what publishes, such as the website and the release. Lint runs in the pull requests, the secret scan and the fuzzing there and every week, and the SBOM every week; every release has an SBOM of its own.
  • The release pull request changes only the changelog and the version, which can't change what a scanner finds. In the blueprint, the required check variants stands for the variants and updates of every kind of project: it passes when each passed, or when none had to run, as in the release pull request.
  • The newest Python, beta included, runs the checks of a Python project after every change of main and every week, in a workflow of its own that keeps the CI of main green. A new Python breaks every pull request alike, and none waits for it. A failure keeps an issue open, which the weekly run closes once the checks pass again.
  • The runners are named with their version, ubuntu-24.04, not ubuntu-latest: GitHub moves that label to a new image one job at a time over weeks, as to Ubuntu 26.04 from 19 October 2026, so that the runs of one workflow would land on either image at random, with another Python, ShellCheck and Docker, and fail like a flaky test. In the blueprint, the Next runner workflow runs the variants on the next runner every week, and a failure keeps an issue open; once they pass there, a release of the blueprint moves every job to it. A workflow of your own, which an update leaves alone, keeps the runner it names: with ubuntu-latest, it moves with GitHub's label.

When a check fails or stops

A check that a runner, a site or GitHub fails for a moment, or that a race of two runs cancels, needs no person: the bots after the checks run it once more, and leave red only what fails for a reason.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: When a check fails or stops
    accDescr: When a run of the checks or of the release ends, the bots after the checks look at how. Tests that failed or ran out of time at first run their failed jobs once more, unless the same jobs broke for the commit before; a job that passes then goes into the issue of flaky tests with its step, its test and its error, and one that fails again stays red. A required check that a late run for an older commit cancelled starts again. The release of a merge that failed runs its failed jobs once more, where they stopped.

    run["A run of the checks<br>or of the release ends"]
    tests["Tests failed or ran out of time<br>at the first attempt"]
    raced["A required check cancelled<br>by a late run for an older commit"]
    release["The release of a merge failed"]
    before{"The same jobs broke<br>for the commit before?"}
    again["The failed jobs run once more"]
    flaky["`Issue *Flaky tests*<br>job · step · test · error`"]
    restart["The run for the head<br>starts again"]
    resume["The failed jobs of the release<br>run once more, where they stopped"]
    red["Stays red, for you"]

    run --> tests
    run --> raced
    run --> release
    tests --> before
    before -- "no" --> again
    before -- "yes" --> red
    again -- "passes" --> flaky
    again -- "fails again" --> red
    raced --> restart
    release --> resume

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class again,restart,resume bot
    class red person
    class flaky done
  • Once, not again and again: the flaky-test bot runs the failed jobs of the first attempt of a test run once more, and so does the bot for failed releases with a release that a merge started; the bot for cancelled checks starts a run twice at most. A job that GitHub stops at its time limit ends cancelled, not failed, and runs once more as well. A run that you or a newer run cancelled stays cancelled, even when a job that waits for the others failed because of it.
  • A failure that repeats is real: when the same jobs broke for the commit before on the same branch, after they ran once more, the jobs don't run again, so that a broken test doesn't keep a runner busy for nothing.
  • The issue of flaky tests gets a table for each run that passed when it ran again: each job that failed at first, with a link to its log, the step that broke, the tests that pytest names in the log, and the first error. A name or a line of a log is written as plain code, so a pull request can't end the table or mention anybody with it.
  • The quick checks run once for each commit: the title and the changelog, the dependency review, the secret scan, the area labels and the website. A late run for an older commit, such as one that an edit of the release pull request starts while release-please pushes to it, no longer cancels them for the head. The checks that take minutes, the tests, Lint, CodeQL and the variants, still cancel the run for an older commit; the bot for cancelled checks repairs a race there.
  • Other workflows of tests that a repository has, such as visual tests, join the CI and Docker E2E with copier update --data "test_workflows=Visual check, Screenshot bot".
  • The link check of a pull request reads the documents that it changes, and every document when it changes the rules, or moves or deletes a file that a document may link to. lychee keeps the answers of the sites for a day and asks again for a link that failed, so that a site that is down for a moment fails only the pull requests that link to it.

The PR coach

A check that fails explains itself, to you and to everyone who sends a pull request, also from a fork: what it found, why that matters and how to fix it, with the commands to run.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: The PR coach
    accDescr: A push to a pull request, also one from a fork, runs the checks. A check that fails explains itself in its summary and as its annotation, from the explanations of .github/checks.toml and .github/checks.project.toml. After the checks, the PR coach reads the check runs and their annotations from the API, without running any code of the pull request, and keeps one comment on the pull request up to date: a section for each check that failed, with the problem, why it matters, the fix and its commands. You push the fix, and once every check passes, the comment says so.

    push(["A push to a pull request<br>also from a fork"])
    checks{"Every check<br>passes?"}
    explain["The check that failed explains itself<br>in its summary and as its annotation"]
    coach["The PR coach after the checks<br>reads the check runs from the API<br>and runs no code of the pull request"]
    comment["One comment on the pull request<br>for each check: the problem, why it matters,<br>the fix and its commands"]
    passed["The comment says<br>every check passed"]
    catalog[(".github/checks.toml<br>.github/checks.project.toml")]

    push --> checks
    checks -- "no" --> explain --> coach --> comment
    comment -- "you push the fix" --> push
    checks -- "yes" --> passed
    catalog -.-> explain
    catalog -.-> coach

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class explain,coach,comment bot
    class push person
    class passed done
  • In the check: a job that fails ends with a step that writes the explanation of its check into the summary and as an annotation, which the list of the checks shows. scripts/check.sh does the same for the step that stopped it, on your machine as well, and in the CI the files of the pull request show the problems of the text files, of Ruff and of actionlint at their lines.
  • One comment, also for a fork: the runs of a pull request from a fork can't write to it, so the coach runs after the checks, in the repository itself. It keeps one comment with a section for each check that failed, the findings of its annotations and a link to its log, and a suggested title when the title is no Conventional Commit. A pull request whose checks never failed gets no comment; once every check passes, a comment that listed failures turns into a short all-clear.
  • Safe with any pull request: the coach reads the check runs and their annotations from the API and checks out only the explanations of the default branch; it runs no code of the pull request. Text that a pull request controls, such as a file name, a message or a title, appears only as code, cut to a short length, so it can't mention anybody, link anywhere or break the comment.
  • Every check explains itself: the explanations are in .github/checks.toml, which the blueprint keeps current, and those of a project's own checks in .github/checks.project.toml, which belongs to the project and wins. A test of the blueprint fails for a required check or a step of scripts/check.sh without an explanation, and python3 .github/blueprint.py coach verify lists them in a repository.

Pull requests fix themselves

Fix first, explain second: what a command can fix of a check that failed, the pull request fixes by itself, also a pull request of Renovate, and main too.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: Pull requests fix themselves
    accDescr: A check fails. Where a command fixes what it found, such as the format of the code, the whitespace of a text file or the rules of markdownlint, the job of the check runs the commands of autofix from .github/checks.toml and keeps the files that they changed. After the checks, the fix bot takes these files as data, only text files that the commit has: on a pull request from a branch of the repository, also one of Renovate, it commits them as one commit onto the branch, and the checks run again; on a pull request from a fork, it suggests them at their lines; on main, it keeps one pull request from bot/self-heal that merges itself. What no command fixes, the PR coach explains.

    failed(["A check fails"])
    command{"A command<br>fixes it?"}
    fix["The job of the check runs the commands of autofix<br>and keeps the files that they changed"]
    bot["The fix bot after the checks<br>takes only text files that the commit has"]
    branch["A branch of this repository, also Renovate's:<br>one commit, and the checks run again"]
    fork["A pull request from a fork:<br>suggestions at their lines"]
    main["main: one pull request from bot/self-heal<br>that merges itself"]
    coach["The PR coach explains<br>what no command fixes"]
    green(["Every check passes"])
    catalog[(".github/checks.toml<br>autofix")]

    failed --> command
    command -- "yes" --> fix --> bot
    command -- "no" --> coach
    bot --> branch
    bot --> fork
    bot --> main
    branch --> green
    main --> green
    fork -- "you commit the suggestions" --> green
    catalog -.-> fix

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class fix,bot,branch,fork,main,coach bot
    class failed person
    class green done
  • The check fixes: each explanation of .github/checks.toml says in autofix which commands fix what its check finds, or why no command can; a test of the blueprint fails for one that doesn't say. When a check fails, its job runs those commands on the head of the pull request, with the tools that it installed already: the text files, the pages in one column and Ruff or php-cs-fixer for scripts/check.sh, markdownlint for the Markdown. It keeps the files that they changed as an artifact.
  • One commit on a branch of the repository: once the checks of the commit that fix have ended, the fix bot commits all their fixes as one commit onto the branch, as the release app, with its sign-off, and the checks run again; the comment of the PR coach names the files. This works on the branches of Renovate and the other bots as well, so that an update of a formatter stays green and merges itself.
  • Suggestions on a fork: no bot pushes to a fork, so the bot suggests the fixes at their lines of the pull request, ten at most in one review and none twice; Commit suggestion takes one. A fix that no suggestion shows, such as line endings, names its file, and the PR coach the command.
  • main heals itself: when a check of main fails with fixes, one pull request from bot/self-heal holds them and merges itself once every check passes; the bot closes it once main needs none, and a failure of the bot keeps one issue open.
  • Safe with any pull request: the bot runs nothing of a pull request with a token that can write. It takes the fixes as data, with blueprint.py of main, and lets through only text files that the commit has already, by plain paths, outside .git and the workflows. It commits at most twice in a row (once more for a check that ends later), never a third time, and a pull request with the label no-bot-fixes gets no commit.
  • On your machine: python3 .github/blueprint.py fix run runs the commands of every step of scripts/check.sh, and python3 .github/blueprint.py fix text fixes the text files alone.

Diagrams that fix themselves

The GitHub app, HACS, the Nextcloud App Store, PyPI and the e-mails of GitHub draw no Mermaid and show a mermaid block as code. So every diagram is a picture of its source, and the bots keep it that way without a person.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How the diagrams fix themselves
    accDescr: A pull request that adds a mermaid block or changes a diagram, and Renovate with a new version of mermaid-cli, run the check diagrams, which draws the diagrams anew. On a branch of the repository, the diagram bot commits the sources and pictures onto the branch, and the checks run again; a fork keeps the failing check, which says the command. After the merge, and every week, main is drawn too: when a diagram is not yet a picture of its source, one pull request from bot/diagrams fixes it and merges itself. So every reader sees every diagram.

    change["`A pull request adds a *mermaid* block<br>or changes a diagram`"]
    renovate["Renovate brings<br>a new mermaid-cli"]
    check{"The check diagrams:<br>every diagram a picture<br>of its source?"}
    bot["The diagram bot commits<br>sources and pictures<br>onto the branch"]
    fork["A fork: the check<br>says the command"]
    merged["The pull request merges"]
    main{"main, after every change<br>and every week:<br>every diagram a picture?"}
    fix["`One pull request from<br>*bot/diagrams* that<br>merges itself`"]
    done["Every reader sees<br>every diagram"]

    change --> check
    renovate --> check
    check -- "no, a branch" --> bot
    bot -- "checks again" --> check
    check -- "no, a fork" --> fork
    check -- "yes" --> merged --> main
    main -- "yes" --> done
    main -- "no" --> fix --> done

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class bot,renovate,fix bot
    class change,fork person
    class done done
  • A pull request fixes itself: when a pull request from a branch of the repository adds a mermaid block or changes a source, the check diagrams runs bash scripts/diagrams.sh --extract, and the diagram bot commits the sources and pictures it made onto the branch, as the release app, with a comment that names the pages it changed. The checks of that commit pass. A pull request from a fork gets the failing check, whose summary lists the files and the command.
  • main fixes itself: after every change of main that touches a diagram or adds a mermaid block, and every week, the Diagrams workflow draws every diagram of main. When something is missing, such as the blocks of a repository right after the update of the blueprint, it keeps one pull request from the branch bot/diagrams with the fix, which merges itself once every check passes, and closes it when main is fixed otherwise. A failed run opens an issue, which the weekly run closes once the bot passes again.
  • A new renderer draws anew: Renovate keeps the image of mermaid-cli in scripts/diagrams.sh current; the check draws the pictures again on its branch, and the diagram bot commits them, so the update still merges on its own.
  • Nothing but diagrams: the bot runs no code of a pull request. It commits only sources in docs/diagrams/, SVG pictures in docs/images/diagrams/ and pages of Markdown, by plain paths, onto the very commit that the check drew, and only for a branch of the repository.

Mutation tests

The mutation tests show the lines that the tests run but don't check: every Saturday they change the code in thousands of small ways, the mutants, and run the tests that cover each one. A mutant that no test notices survives.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How the mutation tests run
    accDescr: Every Saturday, on demand, and for a pull request that changes how they run, the Mutation workflow runs the mutants. mutmut changes the Python in a copy of the repository and leaves out the tests marked mutation_unsafe; Infection changes the PHP of a Nextcloud app, with the bootstrap of its tests. When mutants ran, the summary shows the score and the surviving mutants, the artifact keeps them and the dashboard shows the score. A run that measured nothing fails and shows the last words of the tool.

    start["Every Saturday · on demand<br>a pull request that changes<br>mutation.yml or mutmut"]
    python["mutmut<br>the Python, in a copy in mutants/<br>without the tests marked mutation_unsafe"]
    php["Infection<br>the PHP of a Nextcloud app<br>with tests/bootstrap.php"]
    score["The summary and the artifact<br>the score · the surviving mutants"]
    dashboard["`The dashboard<br>*mutants caught*`"]
    fail["The run fails<br>with the last words of the tool"]

    start --> python
    start --> php
    python -- "mutants ran" --> score
    php -- "mutants ran" --> score
    score --> dashboard
    python -. "nothing measured" .-> fail
    php -. "nothing measured" .-> fail

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class python,php bot
    class score,dashboard done
  • The score is the share of the mutants that a test noticed, among those of the code that the tests run: for mutmut the killed among the killed and the survived, for Infection its Covered Code MSI. The dashboard shows it as mutants caught.
  • A low score fails nothing, not even below a minimum that the project's own configuration sets, but a run in which no mutant ran does, with the end of the output of mutmut or Infection in its summary: a broken configuration shows up in the week it breaks, not as an empty dashboard.
  • A pull request that changes how the mutants run, mutation.yml or the pinned mutmut, tries them, mutmut for a quarter of an hour, and shows a defect as a red check before the weekly run. The check isn't required, so a pull request that merges itself, such as an update of mutmut, doesn't wait for it: its red check is a warning to read.
  • mutmut runs the tests in a copy of the repository, mutants/, with the changed code and without Git. A test that can't work there, because it reads the code as text or asks Git, gets the marker @pytest.mark.mutation_unsafe, and the mutation tests leave it out; the pyproject.toml of a new Python project registers the marker.
  • A configuration of the project wins: a [tool.mutmut] table in pyproject.toml or a [mutmut] section in setup.cfg for mutmut, an infection.json5 for Infection. Without one, mutmut mutates custom_components/, src/ or the modules and packages of the root, and Infection mutates lib/ on PHP 8.3, with tests/bootstrap.php, which loads the classes of Nextcloud.

When a bot fails

GitHub mails the failure of a scheduled run only to whoever last changed its schedule, and that of another run to whoever started it, in these repositories the release app for most runs: a red bot used to show only in the Actions tab, as when Findings, Mutation and Renovate failed for days. The bot-health bot (bot-health.yml) makes each such failure an issue, and closes it once the bot passes again.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: What the bot-health bot does when a bot fails
    accDescr: When a run of a bot or a scheduled workflow ends on the default branch, the bot-health bot looks at it; runs of pull requests start nothing. A first failure that a second try can fix runs its failed jobs once more. A failure that stays opens one issue for the workflow, or updates it, with the failed jobs, their steps and their first errors. A run that passes closes the issue. Every night, the bot looks at the latest run of every workflow, in case GitHub lost an event, and starts a workflow whose schedule GitHub missed or switched off.

    run(["A run of a bot ends<br>on main"])
    nightly(["Every night<br>the latest run of every workflow"])
    passed{"Passed?"}
    first{"A first failure<br>that a second try can fix?"}
    again["Runs the failed jobs<br>once more"]
    issue["One issue for the workflow<br>the failed jobs · their steps<br>their first errors"]
    closed["Closes the issue<br>passes again in the run"]
    missed["Starts a workflow<br>whose schedule GitHub missed"]

    run --> passed
    nightly --> passed
    nightly -- "a schedule missed<br>or switched off" --> missed
    passed -- "yes" --> closed
    passed -- "no" --> first
    first -- "yes" --> again
    again -. "its end" .-> run
    first -- "no" --> issue

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class run,nightly,again,missed bot
    class issue person
    class closed done
  • One issue per workflow, titled like Findings fails on main, with the labels bot-failure and maintenance. It names the latest failed run, its attempt and its trigger, and a table of the failed jobs with the step that failed and the first error of each, from the annotations of the run. A later failure in the same place only updates it, without a notification; a failure in another job or step adds a comment. The next run that passes closes it with a link to that run.
  • A second try only where it can pass: a first failure runs its failed jobs once more, as the flaky-test bot does for the tests, unless another bot already did. Findings, which fails while an alert is open, Mutation, which takes hours to measure the same again, and the property tests and the fuzzing, which failed on an input that they found, are reported at once. A run that GitHub couldn't start, because its workflow file is invalid, too.
  • Which runs count: those of the bots and scheduled workflows that bot-health.yml lists, on the default branch, from a schedule, a push, a start by hand or another workflow, never from a pull request or a fork. The checks that run in pull requests, such as the CI, the website or CodeQL, and the workflows of the project count in the nightly look, which takes the latest finished run of every workflow on the default branch, so that none of their runs for a pull request starts a run of the bot. GitHub gives a run that the end of a check of a pull request starts, as it starts After the checks, the default branch too, so the nightly look counts only the other runs of such a workflow. The Diagrams workflow, Newest Python and the Next runner keep an issue of their own.
  • Missed schedules: GitHub runs a schedule late at busy times, sometimes not at all, and switches the schedules of a repository off after 60 days without activity. Every night, a workflow whose last run is older than the latest time of its schedule, six hours ago or more, starts by hand, at most four a night so that the pull requests keep their runners; Findings, Issue lifecycle and the Issue assistant sync of the blueprint start with dry_run off, as their schedule runs them. A schedule that GitHub switched off is switched on again.
  • Renovate's problems: Renovate runs centrally in the blueprint and logs a lock file that it can't read as a warning, without failing. The Renovate workflow keeps one issue, Renovate logs problems, with the warnings and errors of its latest run that had any, by repository, and closes it after three runs in a row without one. A public issue names public repositories only.
  • The bot reads runs, jobs and annotations and writes issues with the GITHUB_TOKEN of its job, and checks out only the default branch, never the code of a pull request. A repository whose .github/labels.toml, which belongs to the project, lacks bot-failure gets the label from the bot; add it to the file to keep its color and description in one place.

Kinds of projects

Kind Stack Checks Delivery
Home Assistant integration Python 3.14, pytest-homeassistant-custom-component Ruff, mypy, tests, HACS, hassfest a zip asset for HACS
Nextcloud app PHP 8.2, Composer php-cs-fixer, Psalm with its taint analysis, PHPUnit with every line covered signed package to the App Store
GitHub Action Python 3.12 of the runner, composite action Ruff, mypy, tests, a run of the action release tags and a moving major tag
Python package Python 3.12, Hatchling Ruff, mypy, tests, build PyPI, trusted publishing
Node package Node 24, TypeScript tsc, node:test with full coverage, pack npm with provenance
Container image Alpine by digest Hadolint, build, self-test GHCR, multi-arch, with SBOM and provenance
Repository without a build text files, shell scripts releases only

Every kind starts with a small working example and its tests: replace it with the real code.

Start a repository

Copier 9.4 or later, Git, the GitHub CLI and a checkout of the blueprint (for blueprint.py):

copier copy gh:Dennis-Otto/repo-blueprint my-project
cd my-project
git init && git add --all && git commit --signoff --message "chore: start from the blueprint"
gh repo create Dennis-Otto/my-project --public --source . --push
python3 ../repo-blueprint/blueprint.py settings apply
python3 ../repo-blueprint/blueprint.py checklist

Copier asks for the name, a one-sentence description, the kind of project, the license (MIT, MIT-0, BSD-3-Clause, Apache-2.0, GPL-3.0-or-later or AGPL-3.0-or-later) and a few details of the kind. settings apply sets everything the API can set; checklist shows what is left for a person, such as the secrets, and which steps are done.

copier copy in the terminal: Copier clones the blueprint and asks for the name Paperless Sync, the repository, a description and the kind of project, Python package, takes the defaults for the rest, writes the files and names the next steps; the new repository has its community files, its checks and its workflows

Once per repository

  • The release app opens the release and update pull requests, so that their checks run. Install it on the repository, and set its private key as the secret RELEASE_AUTOMATION_PRIVATE_KEY of the environment release. The app needs the permissions Actions, Administration, Checks, Commit statuses, Contents, Issues, Pages, Pull requests, Variables and Workflows (read and write), and Dependabot alerts, Environments and Secrets (read): with them the settings bot applies the settings as code, Renovate updates the dependencies, and the rollout of the blueprint starts the blueprint bot of every repository after a release. Renovate gets a token without Administration. The release workflow creates every release tag with the app, and the ruleset Release tag creation lets nobody else create one; the variables RELEASE_AUTOMATION_CLIENT_ID and RELEASE_AUTOMATION_APP_ID in .github/repository.toml name the app, and a project with an app of its own sets both in .github/repository.project.toml.
  • The issue assistant needs CLAUDE_CODE_OAUTH_TOKEN in the environment issue-assistant.
  • Delivery: trusted publishing on PyPI or npm, the app certificate of a Nextcloud app, or the HACS submission, as the checklist says.

checklist prints the gh secret set commands; the value is typed into the prompt of the GitHub CLI and never appears anywhere else.

An existing repository

An existing repository takes the blueprint on a branch, in one pull request:

  1. copier copy --overwrite --vcs-ref vX.Y.Z --data sample_code=false gh:Dennis-Otto/repo-blueprint . with the answers that fit the repository, without the sample code and tests of a new one, such as its description, topics and homepage as they are on GitHub, so that settings apply changes nothing there. The files of the project (README, changelog, code, tests, manifests, labels, issue forms, icons) stay as they are.
  2. Look at the diff of every file of the blueprint and put back what belongs to the project only: sections of SECURITY.md or CONTRIBUTING.md, rules of Renovate (in .github/renovate.json5), hosts of the issue assistant, ignore rules. copier update keeps these changes from then on.
  3. Write the version of the latest release into version.txt and .release-please-manifest.json, start CHANGELOG.md with ## Unreleased, and give .github/labels.toml the labels of the bots (autorelease: pending, autorelease: tagged, merge-conflict, maintenance, bot-failure, docker).
  4. Put the checks of the project alone, such as its end-to-end tests, into .github/repository.project.toml and scripts/check-project.sh; remove the workflows, scripts and tests that the blueprint replaces, and delete files of the template that the project doesn't need, such as a sample test. A repository without docs/ has no website until it writes docs/index.md; mkdocs.yml shows how German pages join it.
  5. Open the pull request. Once its new checks pass, blueprint.py settings apply switches the required checks, the variables and the labels, and the pull request can merge.

The configuration of the checks belongs to the blueprint and looks for the code where a new project of the same kind has it. Every file it names comes with the blueprint, also without the sample code:

Kind Code Tests From the blueprint
Nextcloud app lib/ PHPUnit tests in tests/Unit/ psalm.xml and phpunit.xml, with the tests/bootstrap.php and tests/stubs/ that they load
Node package src/, built into dist/ test/, typechecked by tsconfig.json; the npm test of a new project runs test/**/*.test.ts tsconfig.json and tsconfig.build.json, which need typescript and @types/node in the package.json of the project

The other kinds set these places in files of their own, such as pyproject.toml. What lies in tests/ belongs to the project: the copy and every update add only the files that are missing there, so an app with its own bootstrap keeps it.

Settings as code

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: The settings as code
    accDescr: The settings of a repository live in .github/repository.toml, with those of the project alone and the labels. blueprint.py settings apply, run by an administrator, and the settings bot, after every change and every week, make GitHub match them; settings check shows what differs.

    files[".github/repository.toml<br>repository.project.toml<br>labels.toml"]
    apply(["settings apply<br>by an administrator"])
    bot(["Settings bot<br>after every change<br>and every week"])
    github["GitHub<br>merges · rulesets · security<br>Actions · environments<br>variables · labels · Pages"]

    files --> apply --> github
    files --> bot --> github
    github -. "settings check<br>shows what differs" .-> files

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    class bot bot
    class apply person

.github/repository.toml holds the settings of a repository; blueprint.py settings check compares them with GitHub and settings apply makes GitHub match:

  • Repository: squash merges with the title of the pull request, auto-merge, branch updates, deleted branches, signed-off web commits, no wiki or projects.
  • Security: immutable releases, private vulnerability reporting, Dependabot alerts with the security updates of Renovate, secret scanning with push protection.
  • Actions: read-only tokens by default, no approvals by workflows, actions pinned to commit hashes, approval for the workflows of every outside contributor.
  • Rulesets: Protect main (pull requests only, squashed, linear history, every required check, no deletion or force push), Release tags (v*.*.* never moves) and Release tag creation (only the release app, whose App ID RELEASE_AUTOMATION_APP_ID names, creates a v*.*.* tag, which the release workflow signs).
  • Variables and environments: PUBLISH_TO, the release app (RELEASE_AUTOMATION_CLIENT_ID and RELEASE_AUTOMATION_APP_ID), and environments that only main may use, with the secrets they need.
  • Pages: GitHub Pages publishes the website that the Docs workflow builds.

blueprint.py needs Python 3.12 and the GitHub CLI signed in as an administrator, and nothing else. It never reads or prints the value of a secret. Every repository has a copy in .github/blueprint.py and .github/blueprint/, and the settings bot (settings.yml) compares its settings with the settings as code every week and after every change of them, so that no repository drifts.

Updates

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How an update of the blueprint arrives
    accDescr: Right after every release of the blueprint, and every week, the blueprint bot verifies the latest published release first. A release that is not immutable, whose tag is not on the main branch of the blueprint or that the release workflow of the blueprint didn't sign fails the run, and nothing is updated. Otherwise the bot runs copier update to that release. When its pull request is still open and you have committed to its branch, the bot leaves the branch to you and says once that a newer release is out. Otherwise it opens the pull request, or moves the open one on to the new release. With conflicts, which turn auto-merge off, or with BLUEPRINT_AUTOMERGE set to off, it waits for the maintainer; otherwise it merges itself once every check passes.

    bot(["Blueprint bot<br>after every release<br>and every week"])
    verified{"Release immutable,<br>on main, signed by<br>its release workflow?"}
    refused(["Fails the run<br>nothing is updated"])
    yours{"Your commits<br>on its open<br>pull request?"}
    note(["Leaves it to you<br>and says once that<br>a newer release is out"])
    pr["Pull request<br>with the changes<br>the open one moves on"]
    conflicts{"Conflicts?"}
    off{"Automerge<br>off?"}
    checks{"Every check<br>passes?"}
    merged["Merges itself"]
    wait(["Waits for<br>the maintainer"])

    bot -- "the latest<br>published release" --> verified
    verified -- "no" --> refused
    verified -- "yes: copier update" --> yours
    yours -- "yes" --> note
    yours -- "no" --> pr --> conflicts
    conflicts -- "no" --> off
    off -- "no" --> checks
    checks -- "yes" --> merged
    conflicts -- "yes: marked in the files<br>auto-merge off, labeled" --> wait
    off -- "yes" --> wait
    checks -- "no" --> wait

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class bot bot
    class wait,note,refused person
    class merged done

The blueprint bot (blueprint-update.yml) runs copier update every week, and right after every release of the blueprint, whose Rollout workflow starts it in every repository of the release app. It takes the latest published release of the blueprint that .copier-answers.yml names, a fork of it perhaps, never a tag whose release is still a draft. Before it updates, it verifies the release: the release must be immutable, its tag must point at a commit of the blueprint's main no older than the version the repository has, and the release workflow of the blueprint must have signed the tag (gitsign, as SECURITY.md shows). Any other release fails the run before Copier renders anything, with the reason and what to do in the summary of the run, so a tag that someone else pushed reaches no repository; when GitHub or Sigstore doesn't answer, the bot asks three times and then waits for its next run. A blueprint that isn't on GitHub can't be verified, so the bot doesn't update from one. The bot keeps one pull request on the branch blueprint/update: a newer release moves that pull request on, with its number and its auto-merge, instead of replacing it. Without conflicts it merges itself once every check passes. Conflicts stay in it as markers for the maintainer, in the files that the update touched; they turn its auto-merge off and add the label merge-conflict. The repository variable BLUEPRINT_AUTOMERGE set to off makes every update wait for the maintainer. Run it by hand under Actions → Blueprint update, or locally with copier update.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How a release of the blueprint rolls out
    accDescr: Once the Release workflow has published and verified a release, the Rollout workflow takes the repositories of the release app one after the other, the quickest checks first. It starts the blueprint bot of a repository and waits up to 15 minutes for the bot and the checks of its update pull request, then goes on with the next one. A repository whose bot it can't start, or whose bot fails, fails the run by its name, and the others still get the release.

    release(["Release workflow<br>publishes and verifies<br>a release"])
    next["Next repository<br>the quickest checks first"]
    start["Starts its<br>blueprint bot"]
    wait["Waits up to 15 minutes<br>for the bot and the checks<br>of its pull request"]
    failed(["The run fails<br>by the name of<br>the repository"])
    more{"Another<br>repository?"}
    finished["Every repository<br>has the release<br>in a pull request"]

    release --> next --> start --> wait --> more
    start -- "GitHub fails<br>three times" --> failed
    wait -- "the bot fails" --> failed
    failed -. "the others<br>go on" .-> more
    more -- "yes" --> next
    more -- "no" --> finished

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class release,next,start,wait bot
    class failed person
    class finished done

The Rollout workflow of the blueprint brings each release to the repositories of the release app one after the other, those with the quickest checks first: it starts the blueprint bot of a repository and gives the bot and the checks of its update pull request up to 15 minutes before it starts the next one, so that the update pull requests don't all wait for the runners of the account at once. A repository whose bot it can't start, or whose bot fails, fails the run by its name; the others still get the release. It rolls out every release that a run of the Release workflow published, whether the merge of the release pull request started that run, the hourly schedule or a start by hand; a release that the run of a later merge finished, because the run of its own merge was lost, waits for a start by hand (Actions → Rollout) or the weekly run of the bots. Started by hand itself, it rolls out the latest release. Rollouts of releases that come close together overlap, but never in the same repository: there, the newer release takes the place of an older one that still waits.

Once you commit to the branch of the pull request, to resolve the markers for example, the bot leaves the branch to you: when a newer release is out, it says so in one comment, and after you merge or close the pull request, its next run brings the rest. A merge of main with the button Update branch doesn't count as your commit, and an auto-merge that you switch off stays off. An update that only records a new release in .copier-answers.yml comes as a pull request too, and the updates carry the modes of files, such as a script that becomes executable.

The blueprint owns the workflows, scripts/check.sh and the community files: their changes arrive with the updates. Files that belong to the project are never overwritten: the README, the changelog, the security design in docs/security.md, the dependency manifests and lock files (Renovate keeps them current), the code and the tests, the website's mkdocs.yml and pages, the issue forms, the labels and their paths, accepted findings, and the notes for coding agents. Checks of a project alone belong in scripts/check-project.sh, which scripts/check.sh runs last.

The issue assistant

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How a release of the issue assistant reaches every repository
    accDescr: Every morning, the sync bot of the blueprint takes the latest published release of the issue assistant and verifies it: the release must be immutable, GitHub's attestation of it must hold, its tag must point at a commit of the main branch of the issue assistant, and its release workflow must have signed the tag. Any other release fails the run, and nothing changes. Otherwise the bot runs install of the release in the root of the blueprint, which writes the four workflows and the firewall policy of the release and moves every pin of the action, and keeps one pull request with the result, which merges itself once every check passes. The next release of the blueprint brings it to every repository with the blueprint bot.

    release(["A release of the<br>issue assistant"])
    bot(["Sync bot<br>every morning"])
    verified{"Immutable, attested,<br>on main, signed by<br>its release workflow?"}
    refused(["Fails the run<br>nothing changes"])
    install["install of the release<br>in the blueprint: the four workflows,<br>the firewall policy, every pin"]
    pr["Pull request from<br>bot/issue-assistant<br>merges itself once<br>every check passes"]
    blueprint["The next release<br>of the blueprint"]
    fleet["Every repository,<br>with its blueprint bot"]

    release --> bot
    bot -- "the latest<br>published release" --> verified
    verified -- "no" --> refused
    verified -- "yes" --> install --> pr --> blueprint --> fleet

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class bot,install,pr,blueprint bot
    class release,refused person
    class fleet done

The four workflows of the issue assistant and its firewall policy are templates of its release, with which the check issue-assistant compares every repository, so a pin of the action that moved alone would fail that check; Renovate leaves the action alone. The sync bot of the blueprint (sync-issue-assistant.yml, in the blueprint alone) takes over each new release instead. Every morning it takes the latest published release of the issue assistant and verifies it as the blueprint bot verifies a release of the blueprint: the release must be immutable, GitHub's attestation of it must hold (gh release verify), its tag must point at a commit of the issue assistant's main, and its release workflow must have signed the tag. Any other release fails the run with the reason in its summary, and nothing changes; so does a release older than the one that the blueprint has, since GitHub calls a release latest by its date, not by its version. Then the bot runs install of exactly the verified commit in the root of the blueprint, takes over the firewall policy of the release, moves every pin of the action to it, that of the check in the Lint workflow too, and gives a workflow that is new in the release the file of the template that renders it in every repository; a release whose install changes anything else fails the run, and the maintainer takes it over by hand. The bot keeps one pull request on the branch bot/issue-assistant, fix(deps): update the issue assistant to vX.Y.Z, which merges itself once every check passes and moves on to a newer release; once someone else commits to its branch, the bot leaves the branch alone until the pull request is merged or closed. The next release of the blueprint, which publishes itself when it holds only updates, brings the release to every repository. Installing runs without secrets; only the job of the pull request has the release app, and it runs nothing of the release. Run it by hand under Actions → Issue assistant sync; with dry_run, it verifies and installs the latest release and shows what would change without a pull request, as it does in every pull request that changes the bot. The bot-health bot (bot-health.yml) keeps one issue open about a failed run, with the reason of a refusal, and closes it once a run passes.

What belongs to a project

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: What belongs to the blueprint and what to the project
    accDescr: copier update brings the changes of the blueprint's files, merged line by line with the changes of the repository, and never overwrites the files of the project. A project adds checks, settings, notes for coding agents and workflows of its own beside those of the blueprint.

    blueprint[("Repo Blueprint")]

    subgraph repo ["Your repository"]
        theirs["`**The blueprint's files**<br>workflows · scripts/check.sh<br>.github/repository.toml<br>community files · AGENTS.md`"]
        mine["`**The project's files**<br>README · CHANGELOG · docs/<br>code and tests<br>manifests and lock files<br>issue forms · labels`"]
        adds["`**What the project adds**<br>scripts/check-project.sh<br>.github/repository.project.toml<br>.github/agents.project.md<br>.github/checks.project.toml<br>workflows of its own`"]
    end

    blueprint -- "copier update<br>merged line by line" --> theirs
    blueprint -. "never overwritten" .-x mine
    theirs -- "joined by" --- adds

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
    class theirs bot
    class mine,adds person

The blueprint keeps the shared parts of every repository equal; a project adds its own without forking them:

  • Changes in the blueprint's files survive updates: copier update merges the changes of the blueprint with those of the repository, line by line, and marks a conflict only where both changed the same lines.
  • Settings of the project alone go into .github/repository.project.toml, such as the checks of its end-to-end tests or an environment with its secrets. blueprint.py and the settings bot add them to .github/repository.toml.
  • Checks of the project alone go into scripts/check-project.sh, which scripts/check.sh runs last; workflows of the project alone are workflow files of their own.
  • Notes for coding agents go into .github/agents.project.md, such as the architecture of the project and the commands of its own checks. AGENTS.md, which the blueprint keeps current, has the rules of every repository and points to them, and CLAUDE.md brings both to Claude Code.
  • Explanations of the checks of the project alone, which the PR coach shows when one fails, go into .github/checks.project.toml; they win over those of .github/checks.toml.
  • Several copyright holders, such as the authors of a fork, are the answer copyright, separated by semicolons; LICENSE and REUSE.toml name each of them.
  • Files under another license, such as third-party artwork, get a .license file next to them, as REUSE describes.
  • The package of a Nextcloud app is checked by scripts/check-package.sh ARCHIVE unsigned|signed if the project has one: scripts/check.sh runs it on the package that krankerl builds, the release on the package before and after signing.
  • The version in other lines of a file of the release, such as the tag in the URL of a screenshot in appinfo/info.xml, follows each release when the line ends with the comment x-release-please-version, such as <!-- x-release-please-version -->.

Tools in every repository

  • bash scripts/check.sh: the checks of the CI, locally.
  • bash scripts/diagrams.sh: draws every diagram of docs/diagrams/ that a page shows as its pictures in docs/images/diagrams/, with Docker, and leaves the other sources alone; --extract first turns every mermaid block of the Markdown into a source and its picture and writes the alt of each picture from its source, and --check checks without Docker what scripts/check.sh checks.
  • bash scripts/social-preview.sh: renders .github/social-preview.html into the 1280×640 image that GitHub shows for links to the repository; upload it under Settings → Social preview.

Dashboard

The dashboard: a table with every public repository of the owner, its kind, the release of the blueprint it is on, its release, its next release, its pull requests and the state of its CI, CodeQL, release, verification, settings, findings and links with its Scorecard, and a second table with the health of each one: coverage, mutants caught, releases in 90 days, the first answer to an issue and the time of the CI

The dashboard shows every public repository of the owner at a glance: the release of the blueprint it is on, its latest release and the pull request of the next one, its open pull requests and those in conflict, the last run of its main workflows on main, and its OpenSSF Scorecard; a second table shows the health of each: the line coverage and the share of mutants that the tests catch, the stable releases and the median time to the first answer to an issue over 90 days, and the median time of the CI, each with an arrow when it moved since a week ago, from the history that the published dashboard keeps. The Dashboard workflow builds it with dashboard.py every six hours and publishes it on GitHub Pages, under the website of the blueprint, which is this documentation. It shows only what is public anyway, no finding of code scanning and no alert of Dependabot.

Every Monday, the Weekly report workflow posts in the discussions what the public repositories of the owner released, merged and fixed in the week: their releases, merged pull requests and the updates among them, opened and closed issues, failed runs on main and the median time of their CI (weekly.py). Like the dashboard, it reports only what is public.

How the blueprint works

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TB
    accTitle: How the blueprint works
    accDescr: Copier renders a new repository from the questions in copier.yml and the files in template/, which take in the workflows of .github/workflows/, the manifests of stacks/ and blueprint.py. Renovate keeps the workflows and the stacks current.

    renovate(["Renovate"])

    subgraph here ["This repository"]
        workflows[".github/workflows/<br>run here and are<br>the templates"]
        stacks["stacks/<br>manifests and<br>lock files"]
        tool["blueprint.py<br>settings · checklist<br>bots"]
        template["template/<br>the files of<br>a new repository"]
        questions["copier.yml<br>the questions"]
    end

    copier(["Copier"])
    repo["A new repository"]

    renovate -. "keeps current" .-> workflows
    renovate -.-> stacks
    workflows --> template
    stacks --> template
    tool --> template
    template --> copier
    questions --> copier
    copier --> repo

    classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
    classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    class renovate bot
    class repo done
  • copier.yml holds the questions. template/ holds the files of a new repository; a file or folder name such as [% if stack == 'php' %]composer.json[% endif %] carries its condition, and a name that renders empty is left out. The delimiters {= =} and [% %] appear in no workflow, script or manifest, so ${{ }} of GitHub Actions and [[ ]] of Bash and TOML stay as they are.
  • The workflows in .github/workflows/ run in this repository and are the templates of the workflows of every new repository: actionlint lints them and Renovate keeps their actions current here. A line marked Not in the blueprint itself switches a job off in this repository and disappears in new ones.
  • blueprint.py starts the tool of the settings as code, the checklist and the bots. Its code is in blueprint/, a module for each task, with a ruff.toml of its own, so that the Ruff settings of a project don't apply to it.
  • stacks/ holds the dependency manifests and lock files of each kind of project, which Renovate keeps current; a new project starts from them.
  • tests/ checks blueprint.py against a simulated GitHub and renders every kind of project; the Variants workflow renders each one and runs its own checks with its real tools, and also updates a repository of each kind from the latest release to the commit, which must bring every file of the blueprint as a new repository has it and pass the same checks: in every pull request but the release pull request, and every week. The Windows workflow runs scripts/check.sh on Windows as well, in Git Bash like a contributor there, every week and for each pull request that changes the code or the tests. The Next runner workflow runs the Variants every week on the runner that the jobs move to next.
  • docs/ makes this documentation the website of the blueprint: its pages include the READMEs, and the Dashboard workflow publishes them with the dashboard under dashboard/.

Where the blueprint is heading: the roadmap. What it protects and trusts: its security design.

Contributions are welcome: see CONTRIBUTING.md. Questions and problems: SUPPORT.md. Report vulnerabilities privately, as SECURITY.md describes.

License

MIT No Attribution: repositories made from the blueprint owe it nothing, not even a notice. Every file names its license in the machine-readable form of REUSE.