Enterprise rollout
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.
- GitHub Actions
- GitHub
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 (opens in a new tab) 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 (opens in a new tab): plotwork-check.yml, plotwork-scan.yml and plotwork-regenerate.yml.
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:
/teams/payments/docs/*.overrides.json @acme/payments/plotwork.config.json @acme/platformA 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. |
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:
- uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14The samples pin the latest release of the Action by its commit. @v1 becomes available at the first stable release.
# .github/dependabot.ymlversion: 2updates: - package-ecosystem: github-actions directory: / schedule: interval: weeklyEach 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:
gh attestation verify dist/index.js --repo plotwork-official/plotwork-actionTo 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, 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 | Sources, overrides, and the Action's inputs |
| Keep diagrams in sync in CI | The other commands of the Action |
| Self-hosting the engine | The engine inside your network |
| Organisations | Organisation keys and the library |