Trusted publishers
A trusted publisher lets a GitHub Actions workflow act for your organisation with no API key in the repository. You name the repository and its workflow file once in the portal; each run asks GitHub for a signed OpenID Connect token, swaps it for a Plotwork token that lives 15 minutes, and reports drift or publishes with it. It works the way trusted publishing works on npm and PyPI.
- GitHub Actions
TL;DR
| Question | Answer |
|---|---|
| Which plan? | Pro and Enterprise, the plans with the drift dashboard. |
| Who sets it up? | The organisation's owner or an admin, under Keys and CI, Trusted publishers. |
| What do I name? | The repository (owner/name), the workflow file (diagrams.yml), and optionally a deployment environment and a branch. |
| What goes in the workflow? | permissions: id-token: write, and the Action step with no PLOTWORK_API_KEY. |
| What can a run do? | What you choose: Publish (diagrams to the library) and Drift (reports to the Drift tab). Never read the audit log. |
| How long does the token live? | 15 minutes. The Action asks for a new one when a run outlives it. |
| What does Plotwork store? | The entry, and a SHA-256 of each token it gave. Never a token, and no secret of yours. |
| Can someone take over the repository name? | No. GitHub's numeric ids for the repository and its owner are pinned, so a repository deleted and made again under the same name is refused. |
| Do I need a GitHub App? | No. |
1. Add the trusted publisher
- In the portal, open the organisation and choose Keys and CI. The address
/orgs/<handle>/ciopens it too. - Under Trusted publishers, choose Add trusted publisher.
- Fill in the repository, such as
acme/platform, and the workflow file name, such asdiagrams.yml. The file name is the one in.github/workflows, with no folder. - Optionally name a deployment environment (only jobs that run in it may sign in) and a branch (
main, or a ref pattern such asrefs/tags/v*). Leave them blank for any. - Choose what its runs can do, and give it a name you will recognise in the usage, the call history and the audit log.
Plotwork asks GitHub for the ids of the repository and its owner when you save. A private repository's id is not public, so it is pinned by the first run instead, and the entry says so until then.
2. Use it in the workflow
Choose Workflow beside the entry to copy a file made for it. It looks like this:
name: diagrams
on: pull_request: push: branches: [main]
permissions: contents: read id-token: write
jobs: diagrams: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: plotwork-official/plotwork-action@86ae8278fa771a422d9df60185182f14b8be4db9 # v1.1.0 with: command: generate mode: check report-drift: trueThere is no env: and no secret. When the Action needs the API and finds no PLOTWORK_API_KEY, it asks GitHub for a token with the API's address as the audience, swaps it, and goes on. A key in PLOTWORK_API_KEY still works, and wins when both are there.
From the release after v1.2.0-beta.1, the output signed-in-as holds the organisation and publisher the run signed in as, such as {"organization":"acme","publisher":"Platform CI"}, and is empty when the run used a key or did not sign in. drift-url holds the Drift tab the run was sent to. A later step can check either.
Signing in by itself needs a release of the Action newer than v1.1.0. With v1.1.0, add this step before the Action's, and it passes the Plotwork token on as PLOTWORK_API_KEY:
- name: Sign in to Plotwork as a trusted publisher run: | id_token=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.plotwork.dev" | jq -r .value) echo "::add-mask::$id_token" token=$(curl -sS -X POST https://api.plotwork.dev/oidc/token -H 'content-type: application/json' -d "{\"token\":\"$id_token\"}" | jq -r .token) echo "::add-mask::$token" echo "PLOTWORK_API_KEY=$token" >> "$GITHUB_ENV"3. What a run can do
| You chose | The run can |
|---|---|
| Publish | Publish generated diagrams to the organisation's library, read the library and the style rules, and report drift |
| Drift | Report drift runs to the Drift tab, and read the style rules |
A run counts as the trusted publisher everywhere: its calls spend the owner's daily allowance and show in the usage and the call history under its name, a diagram it publishes names it as the publisher, and every policy of the organisation applies, including the networks keys may come from, the daily limits, secret scanning, classification and approval.
4. What is checked
Each token is checked against GitHub's published signing keys, and refused unless all of this holds:
| Check | Must be |
|---|---|
| Signature | RS256, by a key GitHub publishes |
| Issuer | https://token.actions.githubusercontent.com |
| Audience | The API's address, such as https://api.plotwork.dev |
| Times | Not expired, already valid, issued in the last ten minutes |
| Repository | The entry's, by name and by GitHub's pinned ids |
| Workflow | The entry's file, as the run's workflow_ref names it |
| Environment | The entry's, when it names one |
| Branch | The entry's pattern, when it names one |
| Reuse | A token is swapped once |
The organisation's audit log records each sign-in as orgs.trusted_publisher.exchange, and a refused one as orgs.trusted_publisher.refused with the reason, at most once an hour for each entry.
5. Troubleshooting
| The run says | Why, and what to do |
|---|---|
| this job cannot ask GitHub for an ID token | The job has no id-token: write. Add it under permissions. A pull request from a fork never gets it; such runs check without reporting. |
oidc.no_trusted_publisher | No entry trusts this run. Check the repository, the workflow file name, the environment and the branch. The audit log's orgs.trusted_publisher.refused event names which did not match. A pull request's ref is refs/pull/<n>/merge, so an entry limited to main refuses pull requests. |
oidc.token_invalid | The token failed a check: usually an audience other than the API's address, as when PLOTWORK_API_URL points at another Plotwork service. Ask for a new token for the right address. |
oidc.token_reused | That token was swapped already. Ask GitHub for a new one. |
oidc.organization_required | More than one organisation trusts this run. Set rules-org, or PLOTWORK_ORG, to the one to sign in to. |
plan.trusted_publishers_not_included | The owner's plan has no trusted publishers. The entries stay and work again after an upgrade. |
auth.token_expired | The run outlived its 15 minutes. The Action swaps again before sending results; a step of your own should do the same. |
auth.key_network_refused | The organisation accepts its keys only from some networks, and GitHub's hosted runners are not in them. Use a self-hosted runner in an allowed network, or add the range. |
oidc.unavailable | GitHub's keys could not be read just now. Run the job again. |
See also
| Page | Why |
|---|---|
| Diagrams in CI | The Action's commands and inputs |
| The drift dashboard | What a drift report sends and where it shows |
| Organisations | Roles, and organisation keys |
| Audit log | The sign-in and refusal events |
| Plans and billing | What Pro and Enterprise include |