# Enterprise rollout

> Page: https://docs.plotwork.dev/enterprise-rollout. Index of every page: https://docs.plotwork.dev/llms.txt

How a platform team puts diagrams as code into hundreds of repositories: one reusable workflow, a required status check, owners for the overrides files, pinned and updated releases, self-hosted runners and GitHub Enterprise Server, an air-gapped setup, and the outputs other tools read. It is for the people who run CI for many teams.

## TL;DR

| Question | Answer |
|---|---|
| How do teams adopt it? | In GitHub Actions, call reusable workflows from a central repository, pinned by commit SHA. [plotwork-examples](https://github.com/plotwork-official/plotwork-examples) has working ones: `plotwork-check.yml`, `plotwork-scan.yml` and `plotwork-regenerate.yml`. |
| What permissions does it need? | Check mode `contents: read`, and nothing else. Write mode `contents: write` and `pull-requests: write`. Code scanning adds `security-events: write`. |
| Is it safe on a fork's pull request? | Check mode is: it reads no secret and makes no network request. Write mode and publishing run on a push or a schedule, never on a fork's pull request. |
| How is the check enforced? | Make the job a required status check in a branch ruleset. |
| How do tools read the result? | The `changes` and `files` outputs (JSON), exit codes, a SARIF file for code scanning, and a JUnit file for a test view. |
| One large repository for many teams? | A settings file per team with `working-directory`, a matrix job, and `since` to check only what a pull request changed. |
| Can we verify a release? | Yes. `dist/checksums.txt` in every release, and from the first release after `v1.0.0-beta.6` a signed build provenance attestation: `gh attestation verify dist/index.js --repo plotwork-official/plotwork-action`. |
| Air-gapped? | The Action and the command line run offline. Only publishing calls an API; the self-hosted engine draws with no outbound request. |

## 1. Reusable workflows

The central repository holds the workflows; each repository calls them. Copies to start from are in [plotwork-examples](https://github.com/plotwork-official/plotwork-examples/tree/main/.github/workflows): `plotwork-check.yml`, `plotwork-scan.yml` and `plotwork-regenerate.yml`.

```yaml
name: diagrams

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 4 * * *'

permissions: {}

jobs:
  check:
    if: github.event_name == 'pull_request'
    uses: acme/platform/.github/workflows/plotwork-check.yml@0123456789abcdef0123456789abcdef01234567
    permissions:
      contents: read
    with:
      changed-only: true

  regenerate:
    if: github.event_name != 'pull_request'
    uses: acme/platform/.github/workflows/plotwork-regenerate.yml@0123456789abcdef0123456789abcdef01234567
    permissions:
      contents: write
      pull-requests: write
    secrets:
      PLOTWORK_API_KEY: ${{ secrets.PLOTWORK_API_KEY }}
```

GitHub checks every job of a called workflow against the permissions its caller grants, even a job that would be skipped, and refuses to start the run when one asks for more. So each reusable workflow asks for one set of permissions:

| Workflow | What it does | Permissions the caller grants |
|---|---|---|
| `plotwork-check.yml` | Fails on drift; writes nothing | `contents: read` |
| `plotwork-scan.yml` | The same, and uploads code scanning alerts | `contents: read`, `security-events: write` |
| `plotwork-regenerate.yml` | Makes the diagrams again and opens a pull request | `contents: write`, `pull-requests: write` |

## 2. Required status checks

Add the check job's name (for the workflow above, `check / Diagrams match their files`) as a required status check in a branch ruleset on the default branch. A pull request whose diagrams drifted then cannot merge until its author runs `plotwork generate` or merges the pull request the nightly run opened.

Keep a nightly run without `since`: a new release of the Action can draw pictures differently, and no file change points at that.

## 3. Owners for the overrides files

Overrides are the only hand-written part of a generated diagram, so review them like code. A `CODEOWNERS` entry sends each change to the team that owns the diagram:

```text
/teams/payments/docs/*.overrides.json   @acme/payments
/plotwork.config.json                   @acme/platform
```

A generated `.diagram` file needs no owner: a hand edit to it is drift, and the check fails.

## 4. Fork pull requests and pull_request_target

| Event | What to run | Why |
|---|---|---|
| `pull_request` | `mode: check` | The token is read-only on a fork and no secret is passed. Check mode needs neither, and makes no request. |
| `pull_request_target` | Nothing from this guide | It runs with the base repository's secrets and a write token. Checking out the fork's head there runs the fork's files with them. Generating diagrams never needs it. |
| `push`, `schedule` | `mode: write` | The branch is the repository's own, so pushing and opening a pull request is safe. |

`on-drift: pr` refuses to run on a pull request, and `on-drift: commit` needs the head branch checked out; neither works on a fork's pull request, by design.

## 5. Many teams in one repository

| Shape | How |
|---|---|
| A settings file per team | `working-directory: teams/payments` finds `teams/payments/plotwork.config.json`. Paths in it start at that folder. |
| A job per team | A matrix over the teams' folders, each job with its own `working-directory`. The reusable workflow's `concurrency` group includes the folder, so teams never wait on each other. |
| Only what changed | `since: ${{ github.event.pull_request.base.sha }}` with `fetch-depth: 0`. A source runs when one of its files, its overrides file, its diagram, or the settings file changed; a deleted file runs every source. |
| `paths` filters | Workflow `paths:` on the files the sources read and the settings file, so a pull request that changes neither starts no job. |
| Many sources | Up to 250 sources in a settings file of at most 256 KB. Past that, split by team. |

```yaml
jobs:
  diagrams:
    strategy:
      fail-fast: false
      matrix:
        team: [payments, search, identity]
    uses: acme/platform/.github/workflows/plotwork-check.yml@0123456789abcdef0123456789abcdef01234567
    permissions:
      contents: read
    with:
      working-directory: teams/${{ matrix.team }}
```

## 6. Outputs other tools read

| Output | Contract |
|---|---|
| Exit code | 0 when every diagram matches its files, 1 on drift or an error (with `fail-on: never`, always 0), and the command line's 2 for a mistake in the command or the settings. |
| `files` | A JSON array of paths from the repository root: the drifted diagrams in check mode, the files written in write mode. |
| `changes` | A JSON array, one object for each drifted diagram, in the order of the settings: `name` (the source's name), `out` (its file from the repository root), `changes` (a count of changes by meaning), `summary` (one sentence). Fields are only ever added, never renamed or removed, within a major version. |
| `published` | A JSON array, one object for each published source: `name`, `kind` (`personal` or `organization`), `url`, `stableLink` (or null), `svgUrl` (an organisation's picture, served to a key; or null), `id`, `organization`. |
| `sarif-file` | SARIF 2.1.0: rule `plotwork/drift` for each drifted diagram on its first line, and one rule for each diagnostic code on its line. Upload it with `github/codeql-action/upload-sarif` and the drift shows in the Security tab. |
| `junit-file` | JUnit XML: one test case for each source, failed on drift or an error. |
| Command line | `plotwork generate --check --format json`, `--format sarif`, or `--format junit` prints the same to standard output. |

## 7. Pinning and updates

Pin the Action by its full commit SHA, with the release in a comment, and let Dependabot move it:

```yaml
- uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14
```

The samples pin the latest release of the Action by its commit. `@v1` becomes available at the first stable release.

```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
```

Each release's `dist/checksums.txt` holds the SHA-256 of the bundle and its notices; `sha256sum --check dist/checksums.txt` in a checkout of the release verifies them.

Every release after `v1.0.0-beta.6` also carries a signed build provenance attestation for `dist/index.js` and `dist/checksums.txt`. The Action's public repository signs it with GitHub artifact attestations when the release is published, after checking the checksums, and records it in the public Sigstore transparency log. In a checkout of the release, with the GitHub CLI:

```bash
gh attestation verify dist/index.js --repo plotwork-official/plotwork-action
```

To require the release workflow and the tag as well, add `--signer-workflow plotwork-official/plotwork-action/.github/workflows/release.yml --source-ref refs/tags/v<version>`. The attestation proves the file is the one in the release tag and was signed by that repository's release workflow. The bundle is built from a private repository, so it does not describe that build.

A release can draw some pictures differently, and then the Dependabot pull request that moves to it makes those pictures stale; merge it together with the regenerated diagrams, which the nightly write run opens as a pull request.

## 8. Runners, GHES, and air-gapped networks

| Setup | What to know |
|---|---|
| Self-hosted runners | The Action runs on the runner's `node24`, with `git` on the path for write mode. It needs no other tool, and no install. |
| GitHub Enterprise Server | The Action reads `GITHUB_SERVER_URL` and `GITHUB_API_URL`, so pushing and pull requests go to the server. Copy the release into the server, or sync it with GitHub Connect. |
| No internet | Check and write mode make no request to Plotwork: the importers and the renderer are in the bundle. Leave `publish` off. The command line (`npx @buun_group/plotwork generate`) runs offline the same way, from an internal npm mirror. |
| Hosted pictures inside the network | The [self-hosted engine](https://docs.plotwork.dev/mcp/self-hosting.md), one container, serves the drawing tools and the `/v1` drawing routes with no outbound request. It has no accounts or hosting, so publishing stays off. |

## 9. Keys and privacy

| Question | Answer |
|---|---|
| Which key? | An organisation key publishes into the organisation's library and needs no other setting. A personal key with an `alias` publishes a stable link. |
| Where does it live? | A repository or organisation secret, passed as `PLOTWORK_API_KEY` to the step that publishes, in write mode only. |
| Can it leak? | It is masked with `::add-mask::` before any request, never printed, and never sent to GitHub. |
| What leaves the runner? | With publishing on, the generated diagram source and the repository's defaults and brand, to the API. The files the diagrams are made from never do. |
| Which API? | `PLOTWORK_API_URL`, or the hosted one. A key works only on the service that made it, and the refusal names the address it called. |

## 10. Failures to plan for

| Situation | What happens |
|---|---|
| A file that does not parse | An annotation on its line, and that source writes nothing. The others still run. |
| A source whose files are missing | S103, on its line of the settings file. |
| An overrides file a source names that is not there | Warning S203; the diagram is made without overrides. |
| A protected branch refuses the push | The error names the rule and says to use `on-drift: pr`, or let the token bypass it. |
| A person pushed to the pull request branch | The action leaves the branch as it is and says so; it never replaces someone else's commit. |
| GitHub rate limits, or a server error | Each request is tried up to four times, waiting as the answer asks. |
| Two runs at once | Give each workflow a `concurrency` group by branch and folder, and never cancel a write. |
| A summary too large | The summary keeps under GitHub's 1 MiB limit, shortening the change tables first; the outputs and the log keep everything. |

## See also

| Page | Why |
|---|---|
| [Diagrams as code](https://docs.plotwork.dev/diagrams-as-code.md) | Sources, overrides, and the Action's inputs |
| [Keep diagrams in sync in CI](https://docs.plotwork.dev/diagrams-in-ci.md) | The other commands of the Action |
| [Self-hosting the engine](https://docs.plotwork.dev/mcp/self-hosting.md) | The engine inside your network |
| [Organisations](https://docs.plotwork.dev/organisations.md) | Organisation keys and the library |
