Screenery

Docs / Reference

Credentials

On GitHub Actions, pushing needs no secret at all. Everything else uses a token you create on the console's Access screen, or a session from screenery login.

The kinds

CredentialLooks likeForLives
GitHub OIDCnothing to storePushing from GitHub Actions5 minutes, minted by each run
Publish tokensk_live_pub_…Pushing and promoting from other CI7, 30 or 90 days, or never
Read tokensk_live_read_…Reading private screenshots7, 30 or 90 days, or never
Embed tokensk_live_read_…A private screenshot in a public pageNever expires
Sessionsk_user_…You, from the CLI or an agent90 days from last use, at most a year

Every token is shown once, when it is created. Screenery cannot show it again. The CLI sends credentials in an Authorization header and never prints one.

GitHub OIDC

OIDC is how GitHub Actions proves which repository and run a job belongs to, with a short-lived token GitHub signs. The screenery/push@v1 Action and screenery push ask the runner for that token and exchange it for a publish token that lives five minutes. There is no secret to store and nothing to rotate.

A large push can take longer than five minutes. Since CLI 0.4.0 the push exchanges again before it starts uploading, and again before it finishes the build if less than a minute is left. Each exchange is one row in the exchange log below. In 0.3.0 the push exchanges once, so a push that runs past five minutes fails with a 401.

It needs three things:

    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - uses: screenery/push@v1
        with: { project: acme/dashboard, path: test-results }

The binding is the repository's numeric id, not its name. Renaming the repository changes nothing, and a fork cannot pass. One repository can feed several projects; the Action's project input picks one. Uninstalling the App stops the exchange at once. Reinstalling it restores publishing.

An OIDC push may move the default branch's channel and @latest only from a job on the default branch. See Which jobs may move it.

Each exchange is listed under OIDC exchanges on the Access screen, with the workflow run that asked for it and, for a pull request, the branch it came from.

Which one a command uses

screenery push, and so the Action, tries two credentials in a fixed order:

OrderCredentialWhen
1GitHub OIDCThe runner offers an OIDC token.
2SCREENERY_TOKENOtherwise. Any CI that is not GitHub Actions.

OIDC wins even when SCREENERY_TOKEN is also set.

A leftover secret therefore stays unused, which makes deleting it safe. If the exchange is refused, the push says so. It then falls back to SCREENERY_TOKEN with a warning, or, with no fallback, fails and tells you to check the repository binding on the Access screen.

Other commands:

CommandCredential
promote The same as push: GitHub OIDC first, then SCREENERY_TOKEN, a publish token. CLI 0.3.0 uses only SCREENERY_TOKEN.
sign SCREENERY_TOKEN, publish or read, to look up visibility. Plus SCREENERY_SIGNING_KID and SCREENERY_SIGNING_KEY, which sign the URL. Since CLI 0.4.0, GitHub OIDC is tried first for the lookup.
login Creates your session.
whoami SCREENERY_USER_TOKEN if set, else the session login stored.
logout The same session, which it revokes.

Fork pull requests

GitHub gives a pull request from a fork no OIDC token. That is deliberate: a fork's workflow must not publish to your project. From inside the job it looks exactly like a workflow that forgot id-token: write, so the CLI reads the event and tells you which one it is.

For a fork, the log says:

error: nothing was published: this run has no credential, and that is expected here.

Nothing is misconfigured. The screenshots publish from your base branch's own run once the pull request merges. For a pull request from the same repository, the CLI prints the permissions block to add instead.

Publish tokens

Use a publish token for CI that is not GitHub Actions, or for a project not bound to a repository. Put it in the environment as SCREENERY_TOKEN, as a secret.

Read and embed tokens

A read token opens private screenshots at cdn.screenery.dev, and reads the project through the API. An admin creates it on the Access screen and must give it a scope:

ScopeMeaning
Asset globWhich names it opens. Required.
Expiry7, 30 or 90 days, or never.
ReferrersOptional. Hostnames such as docs.acme.com or *.acme.com.

The glob matches asset names segment by segment:

PatternMatches
*Any characters inside one segment.
?One character inside one segment.
**Any number of whole segments, including none. checkout/** covers checkout and everything under it.

The referrer list discourages hotlinking. It is not access control, because a client can send any Referer it likes. The token is what keeps a screenshot private.

A read token cannot read project-wide pages. Onboarding status and the OIDC exchange log answer it with 403 forbidden.

Every read token is scoped to a glob, even **, and those answers describe the whole project. Use a signed-in session for them.

An embed token is a read token meant for a public page. It never expires, so a pasted snippet keeps working, and its glob must name at least one literal segment: checkout/*, not *. See Embedding.

The Access screen can revoke one token, or every read token at once. Revoking read tokens does not touch OIDC publishing.

Your session

Creating projects, minting tokens and binding repositories are things a person does. The CLI does them as you:

npx screenery login
npx screenery whoami
npx screenery logout

login prints a console URL and a code. Open the URL while signed in, check the code, and approve it. The CLI polls until you do.

The session is the one credential the CLI writes to disk. It goes to ~/.config/screenery/credentials.json, or %APPDATA%\screenery\credentials.json on Windows, readable only by you. Set SCREENERY_CREDENTIALS_FILE to put it elsewhere, or SCREENERY_USER_TOKEN to use a session without the file.

A session from login lasts 90 days from its last use, and at most a year. whoami shows who you are and your roles. logout revokes the session first and then forgets it. If the revoke fails, the file is kept and the command exits non-zero.

screenery doctor

npx screenery doctor

It checks the machine, not your project: the Node version, the npm launcher, that it can reach app.screenery.dev, that SCREENERY_CDN_URL is valid, and that the session file's folder is writable. Each failed check names its fix, and the command exits 1 if any fails. It reports whether a session exists and where it came from. It never prints the session, and it writes nothing.