# Trusted publishers

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

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.

## 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

1. In the portal, open the organisation and choose Keys and CI. The address `/orgs/<handle>/ci` opens it too.
2. Under Trusted publishers, choose Add trusted publisher.
3. Fill in the repository, such as `acme/platform`, and the workflow file name, such as `diagrams.yml`. The file name is the one in `.github/workflows`, with no folder.
4. Optionally name a deployment environment (only jobs that run in it may sign in) and a branch (`main`, or a ref pattern such as `refs/tags/v*`). Leave them blank for any.
5. 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:

```yaml
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: true
```

There 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`:

```yaml
      - 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](https://docs.plotwork.dev/diagrams-in-ci.md) | The Action's commands and inputs |
| [The drift dashboard](https://docs.plotwork.dev/drift-dashboard.md) | What a drift report sends and where it shows |
| [Organisations](https://docs.plotwork.dev/organisations.md) | Roles, and organisation keys |
| [Audit log](https://docs.plotwork.dev/audit-log.md) | The sign-in and refusal events |
| [Plans and billing](https://docs.plotwork.dev/plans-and-billing.md) | What Pro and Enterprise include |
