Penstock

Command line / Automation

Pipelines, reports and the bot

Run the checks on every push, get a review comment on every pull request, and deploy from CI, all with the same commands you use on your machine.

Free The free check runs anywhere. The bot, reports, baselines and SARIF need Team in a pipeline.

Set up a pipeline in one command

  1. In your project run penstock init --ci github. The other choices are gitea, forgejo, gitlab, bitbucket and azure.
  2. Penstock writes the pipeline files, and tells you what to add in your CI settings (a secret, a token).
  3. Commit the files and push.

The pipeline it writes depends on the licence on the machine that ran the command. Without Team it runs the free check, the drawing and the rules, so its first run does not fail on a feature nobody has bought. With Team it also sets up the review bot, SARIF and baselines. If you buy Team later, run init --ci again. It writes the pipeline files again, over the ones it wrote before, so keep your own changes in a separate file.

PlatformFiles written
GitHub.github/workflows/penstock.yml, penstock-publish.yml, and with Team penstock-bot.yml
Gitea, Forgejo.gitea/workflows/ or .forgejo/workflows/: penstock.yml, or penstock-bot.yml with Team
GitLabpenstock.gitlab-ci.yml. Include it from .gitlab-ci.yml
Bitbucketbitbucket-pipelines.penstock.yml. Merge it into bitbucket-pipelines.yml
Azure DevOpsazure-pipelines.penstock.yml

The check in a pipeline

The free workflow on GitHub looks like this:

name: Diagrams

on: [push, pull_request]

jobs:
  penstock:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - uses: kern0x1b/penstock-release@<commit>
        with:
          command: check

init --ci pins the action to a commit, not to a branch, and writes the commit and its date in the file.

The action runs your project's penstockw when there is one, so CI runs exactly the version the project pins, and it caches the download. Inputs: command (check, layout, fmt, deploy or doctor), paths, profile, version, baseline, sarif and args.

With Team, the same step gets the licence from a secret, writes SARIF against the baseline and uploads it to code scanning:

      - uses: kern0x1b/penstock-release@<commit>
        env:
          PENSTOCK_LICENSE: ${{ secrets.PENSTOCK_LICENSE }}
        with:
          command: check
          sarif: penstock.sarif
          baseline: 'true'

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: penstock.sarif

Get the licence into the pipeline

  1. On a machine that has a licence activated, in a terminal, run penstock license key. It signs in with an e-mail code and prints the Team key.
  2. Store the key as a secret named PENSTOCK_LICENSE.

The key takes no machine's place, and only a Team key works: a Pro key is refused in a pipeline. Penstock uses it only when CI is set, so a laptop that has the variable does not become licensed by it. Each new runner exchanges the key for a token that lasts six hours. If the licence service cannot be reached, the run is Free. See Buying and signing in.

Machine formats

--format sarifSARIF 2.1.0. GitHub code scanning reads it and shows notes on the pull request.
--format githubAnnotation lines for a workflow that does not use code scanning.
--format jsonFor anything else. Every finding carries its rule id and the address of its page.

SARIF and annotations are Pro on your machine and Team in a pipeline, and cover every section of the check.

Only what is new

A first run on a repository that has drawn diagrams for years is red, and a red build nobody can fix gets switched off. Record what the project already lives with, and check against it from then on.

penstock check . --update-baseline   # writes penstock-baseline.json. Commit it.
penstock check . --baseline          # fails only on what is new

It also says what was fixed since the baseline was taken, so you can refresh the file when the backlog shrinks. A baseline covers every section. One taken before the check had sections needs refreshing with --update-baseline. Baselines are Pro, and Team in a pipeline.

The file is JSON: the time it was written, and a list of findings, each with the path of the file relative to the project, the rule id and the message. A finding is the same one when all three match. Changing the text of a message, or moving the diagram to another folder, makes it new. penstock migrate analyze has its own baseline, penstock-migration-baseline.json, written by penstock migrate analyze --update-baseline and read with --baseline, so the two lists of accepted findings do not mix.

Before the commit

penstock init --hooks installs a pre-commit hook that formats and checks the diagrams a commit carries, and nothing else. It lives in .git/hooks, so it is yours, not the repository's. Deleting the file removes it. It runs check --baseline, so it needs Pro.

The pull request report

Pro, Team in a pipeline

penstock report writes one Markdown comment for a pull request: the check against the baseline, what changed by element, and pictures with the changes outlined (green arrived, amber differs).

penstock report . --base origin/main --out penstock-report.md

The review bot

Pro, Team in a pipeline

The bot is penstock bot running in your own CI. There is no service behind it and nothing to register. It answers with the token of the pipeline it runs in.

On a pull or merge request that touches a diagram it leaves one comment, and edits that same comment on every push. In the conversation you can ask it:

/penstock                    the full review
/penstock check              only the problems
/penstock diff v1.4.0        what changed since a commit, branch or tag, outlined
/penstock image orders.bpmn  a picture of the diagram as it is now
/penstock help

The picture is drawn by Penstock itself without a browser. Nothing is ever committed to the repository.

PlatformComments answeredThe pictureWhat to add
GitHubyeskept with the run, opens from the link in the commentNothing
Gitea, Forgejoyesattached to the pull request, shown inlineNothing (or PENSTOCK_TOKEN)
GitLabyes, through a webhook on the pipeline triggeruploaded to the project, shown inlinePENSTOCK_TOKEN: a project access token, role Developer, scope api
Azure DevOpsno, the review onlyattached to the pull request, shown inlineLet the build service contribute to pull requests
Bitbucketno, the review onlya pipeline artifact, linked from the commentPENSTOCK_TOKEN: a repository access token with pull request write

init --ci prints exactly what to add for your platform. The bot reads the diagrams and penstock.yaml. It never runs hooks that a pull request's configuration might declare.

Asking the bot, and the variables it reads

A comment is read when it mentions /penstock or @penstock. After the mention: nothing or report gives the full review, check or problems only the problems, diff or against with a ref what changed since it, and image, picture or screenshot with a .bpmn path a picture. Anything else gives the help. A pull request from a fork gets a refusal, not a run.

VariableWhat it does
PENSTOCK_COMMANDThe request, as text, for a run that was not started by a comment, such as a pipeline on a pull request. Set it to /penstock check and the bot answers with the problems only. When it is not set, the answer is the full review. When it does not mention Penstock, the bot says it is not for it and stops.
PENSTOCK_BOT_DIRWhere the bot keeps what it draws, in a folder called penstock-bot inside it. Without it the bot uses RUNNER_TEMP, and then the system's temporary folder. The folder holds the saved answer, answer.json, and the pictures, stacked in penstock-diagrams.svg. Point it into the workspace when the pictures are to be kept as a pipeline artifact: the pipelines that init --ci writes for Bitbucket and Azure set it.
PENSTOCK_PICTURES_URLThe address that a comment links to for pictures it cannot show inline. By default it is the page of the run: the run on GitHub, the job's artifacts on GitLab, the pipeline's results on Bitbucket, the build's artifacts on Azure.

A run can be split in two with --stage draw and --stage post. The first does everything except posting, and saves the answer and the pictures under PENSTOCK_BOT_DIR. The second posts the saved answer. Use it when the pictures have to be uploaded in between.

Deploy from CI

init --ci github also writes a publishing workflow. On merge it runs penstock deploy . --wait with the prod profile:

PENSTOCK_PROFILE=prod \
PENSTOCK_PASSWORD=$CAMUNDA_PASSWORD \
./penstockw deploy . --wait

The profile chooses the environment. The secret arrives through the environment, not through the repository. With auth.type: oauth the client secret comes from PENSTOCK_CLIENT_SECRET. PENSTOCK_TOKEN is used as a bearer token when the engine wants one. Any PENSTOCK_* variable that matches a setting overrides it for the run, for example PENSTOCK_DEPLOYMENT_URL. --wait matters: it asks the engine for the definitions it just accepted until the engine answers. Run --dry-run first in a new pipeline. See Deployment.

Test cases in CI

Mark the test engine with deployment.forTests: true on its profile. Then penstock scenarios run . --junit-xml build/test-cases.xml runs them. See Scenarios.

What you see

Common problems

The bot does not answer.
Check the token setup that init --ci printed for your platform, and that PENSTOCK_LICENSE holds a Team key. Also check that the job has fetch-depth: 0, so it can compare against the base branch.
The first run of the check is red on an old repository.
Take a baseline with --update-baseline and check with --baseline from then on.
"Findings in the code review ... is part of Penstock Team".
SARIF and annotations in a pipeline need Team. Use the plain check, or add a Team key.
My pipeline downloads Penstock on every run.
Commit the wrapper (penstockw and its properties file). The GitHub action then caches ~/.penstock/wrapper/dists.