Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trusted publishing and OIDC

monochange supports built-in package publishing for the canonical public registries of the ecosystems it manages:

  • Cargo → crates.io
  • npm packages → npm
  • Deno packages → jsr
  • Dart / Flutter packages → pub.dev
  • Python packages → pypi
  • Go modules → go_proxy via VCS tags

For those registries, monochange can also manage or verify trusted publishing when the registry supports publishing directly from a verified GitHub Actions identity. PyPI is supported by the built-in publisher through uv build and uv publish; PyPI trusted-publisher enrollment is still completed manually in the PyPI project settings.

Different registries use different names for the same general pattern:

  • trusted publishing
  • OIDC publishing
  • automated publishing
  • trusted publishers

The goal is the same in every case:

  • publish from CI instead of local machines
  • avoid long-lived registry tokens where possible
  • restrict publish rights to a specific repository, workflow, and sometimes environment

Provider and registry capability matrix

Trusted publishing support is not uniform across registries or CI providers. monochange models these dimensions separately so it can be strict where support is verifiable and honest where setup still needs manual review.

EcosystemRegistryTrusted-publishing providers modeled by monochangeCurrent CI identity can be detectedPublish-time setup/context can be verified by monochangeRegistry-side setup can be automated by monochangeRegistry-native provenance / attestations
npmnpmGitHub Actions, GitLab CI/CDYesGitHub Actions context onlyNo; run npm trust github ... manually or externallyYes, npm provenance
cargocrates.ioGitHub ActionsYesNoNoNo registry-native package provenance
denojsrGitHub ActionsYesNoNoYes, JSR package provenance
dart / flutterpub.devGitHub Actions, Google Cloud BuildYesNoNoNo registry-native package provenance
pythonPyPIGitHub Actions, GitLab CI/CD, Google Cloud BuildYesNoNoYes, PEP 740 digital attestations
goGo proxyNone; VCS tags are used insteadN/AN/ACreates module tagsSource-control provenance only
custom/privatecustomNone by defaultProvider may be detectedNoNoUnknown

monochange also detects CircleCI publish-time identity, but none of the built-in public registry combinations above are treated as CircleCI trusted-publishing support today. Unknown local shells and unsupported providers are never treated as trusted.

npm is the only ecosystem where monochange can render precise GitHub trusted-publisher repair commands. It still does not run npm trust during real package publishing; use mode = "external" for any registry workflow that should stay outside monochange’s built-in publisher.

Go module publishing is included in the built-in package publisher, but it is not an OIDC trusted-publishing flow. Go versions are published by creating VCS tags. monochange uses git tag, choosing v1.2.3 for a root module and path-prefixed tags such as api/v1.2.3 for submodules, then checks availability through the Go module proxy.

For crates.io, jsr, pub.dev, and PyPI, monochange reports the setup URL for each package and blocks the next built-in registry publish until the trust configuration has been completed manually. It also preflights the trusted-publishing context for those registries, surfacing the provider capability message and, for GitHub contexts, the repository, workflow, and environment it expects when that context can be resolved.

monochange configuration

Start by enabling trusted publishing for the relevant ecosystem. Packages inherit the ecosystem publish setting by default and can override it when needed.

[source]
provider = "github"
owner = "ifiokjr"
repo = "monochange"

[source.releases.attestations]
require_github_artifact_attestations = true

[ecosystems.npm.publish]
trusted_publishing = true

[ecosystems.npm.publish.trusted_publishing]
workflow = "publish.yml"
environment = "publisher"

[ecosystems.npm.publish.attestations]
require_registry_provenance = true

[ecosystems.cargo.publish]
trusted_publishing = true

[ecosystems.deno.publish]
trusted_publishing = true

[ecosystems.deno.publish.attestations]
require_registry_provenance = true

[ecosystems.dart.publish]
trusted_publishing = true

[ecosystems.python.publish]
trusted_publishing = true

[package.cli.publish.trusted_publishing]
workflow = "publish-cli.yml"

[package.legacy.publish]
trusted_publishing = false

Use ecosystem publish settings for the shared trust policy and GitHub context. Use package publish settings only for package-specific workflows, environments, or opt-outs.

monochange resolves the GitHub trust context from:

  • publish.trusted_publishing.repository
  • publish.trusted_publishing.workflow
  • publish.trusted_publishing.environment
  • otherwise [source]
  • otherwise GitHub Actions runtime values such as GITHUB_REPOSITORY, GITHUB_WORKFLOW_REF, and GITHUB_JOB

If your workflow filename or environment cannot be inferred reliably, set them explicitly in monochange.toml.

Requiring or preferring trusted publishing

publish.trusted_publishing.mode decides what happens when no verifiable CI/OIDC identity is available:

  • mode = "required" (default). Publishing must run from a verifiable CI identity. Local and manual runs fail before any registry mutation. Use this when trusted publishing is the only publishing path you want to allow.
  • mode = "preferred". Trusted publishing is used whenever a verifiable CI identity is detected, and publishing falls back to local/manual credentials otherwise. Detected CI identities are still verified against the configured repository, workflow, and environment, so a misconfigured CI context fails instead of silently publishing with a token.
[ecosystems.dart.publish.trusted_publishing]
enabled = true
mode = "preferred"
repository = "acme/widgets"
workflow = "publish.yml"
environment = "publisher"

preferred fits repositories that publish from CI with OIDC trusted publishing but also let maintainers run monochange run publish locally with their own registry credentials. The mode only relaxes the identity requirement. It never disables the CI context verification, and enabled = false remains the explicit opt-out from trusted publishing entirely.

Attestation and provenance policy

Trusted publishing and attestations answer different questions:

  • trusted publishing decides which CI/OIDC identity may publish a package
  • registry-native package provenance records where a published package came from in registries that support it, such as npm provenance, JSR provenance, and PyPI PEP 740 attestations
  • GitHub release artifact attestations cover release assets uploaded to GitHub Releases and are separate from package-registry provenance

Trusted publishing does not automatically require registry provenance. Enable provenance explicitly for registries where you want monochange to enforce it:

[ecosystems.npm.publish]
trusted_publishing = true

[ecosystems.npm.publish.attestations]
require_registry_provenance = true

Packages inherit publish.attestations from their ecosystem publish settings. Package-level settings can override or opt out:

[package.legacy.publish.attestations]
require_registry_provenance = false

When publish.attestations.require_registry_provenance = true, built-in release publishing fails before invoking the registry command unless all of these are true:

  1. publish.trusted_publishing = true is effective for the package.
  2. The current environment exposes a verifiable CI/OIDC identity.
  3. The provider/registry capability matrix says registry-native provenance is available for that identity.

Today this is enforceable for npm trusted publishing and JSR publishing from supported OIDC contexts. The capability matrix records PyPI PEP 740 attestations as registry-native provenance, but monochange rejects require_registry_provenance for PyPI until the built-in Python publisher exposes a publish command that can require uploading those attestations. It also rejects crates.io, pub.dev, Go proxy publication, and custom registries because those flows do not expose registry-native package provenance that monochange can require without creating false assurance.

For GitHub release assets, keep the policy under [source.releases.attestations]:

[source]
provider = "github"
owner = "ifiokjr"
repo = "monochange"

[source.releases.attestations]
require_github_artifact_attestations = true

This setting is accepted only for the GitHub source provider. It records that release assets are expected to be covered by GitHub Artifact Attestations; the workflow that builds/uploads assets must still grant attestations: write and run the GitHub attestation action for the uploaded subjects.

GitHub Actions baseline

Most registries require the publish job to request an OIDC token.

permissions:
  contents: read
  id-token: write

If you use a protected deployment environment, keep the workflow and registry settings aligned:

jobs:
  publish:
    environment: publisher

Use the same environment name in GitHub Actions and in the registry configuration.

When publish.trusted_publishing = true, release publishing is a mandatory CI/OIDC flow. Built-in publish commands reject local/manual execution before invoking registry CLIs, require the configured GitHub repository/workflow/environment to match the current job, require id-token: write, and refuse long-lived npm token environment variables such as NPM_TOKEN or NODE_AUTH_TOKEN. Use publish.trusted_publishing = false only for packages that intentionally opt out.

Use this sequence when adopting trusted publishing for an existing workspace:

  1. Set publish.trusted_publishing = true for the target ecosystem, then override individual packages only when they differ.
  2. Run monochange step placeholder-publish --dry-run to see which packages do not exist yet.
  3. If needed, run monochange step placeholder-publish so the package exists in the registry first.
  4. Complete the registry-side trusted-publishing setup for each package.
  5. Run monochange step publish-packages --dry-run to confirm monochange sees the expected trust configuration.
  6. Optionally generate a readiness artifact in CI with monochange step publish-readiness --from HEAD --output .monochange/readiness.json for preflight review or publish planning.
  7. Publish from CI with monochange step publish-packages --output .monochange/publish-result.json.

Placeholder publishing is especially useful when the package name must exist in the registry before trusted publishing can be configured.

npm

Registry-side setup

On npm, trusted publishing can be configured from the package settings page or through the CLI.

UI path

  • npmjs.com → package → Settings → Trusted publishing

Fields to enter for GitHub Actions

  • Organization or user: GitHub owner
  • Repository: GitHub repository name
  • Workflow filename: for example publish.yml
  • Environment name: optional, for example publisher

Only the workflow filename is required, not the full .github/workflows/... path.

CLI setup commands

These are the same commands monochange models for npm trusted publishing.

List the current trusted publishers for a package:

npm trust list <package-name> --json

Configure a package for a GitHub workflow:

npm trust github <package-name> \
  --repo owner/repo \
  --file publish.yml \
  --yes

Add an environment restriction:

npm trust github <package-name> \
  --repo owner/repo \
  --file publish.yml \
  --env publisher \
  --yes

If the workspace uses pnpm, use the pnpm-wrapped form:

pnpm exec npm trust github <package-name> \
  --repo owner/repo \
  --file publish.yml \
  --env publisher \
  --yes

Workflow requirements

At minimum, the publish workflow should have:

permissions:
  contents: read
  id-token: write

monochange notes

  • Real package publishing does not execute npm trust or npm trust list automatically.
  • monochange verifies the configured GitHub Actions OIDC context before trusted npm publishing.
  • If trust needs repair, monochange reports the exact npm trust github ... command to run manually or from separate tooling.
  • Trusted npm publishing uses the npm CLI directly. pnpm workspaces can still use pnpm exec npm trust ... for manual setup commands, and non-trusted pnpm publishing paths continue to use pnpm.

crates.io

Registry-side setup

crates.io supports trusted publishing through CI-issued OIDC. monochange models GitHub Actions for built-in trusted-publishing diagnostics and keeps other crates.io provider combinations manual until registry-side verification is available.

Trusted publishing on crates.io exchanges your CI identity for a short-lived publish token, so you do not need a long-lived crates.io API token in CI.

Prerequisites

  • the crate must already exist on crates.io
  • you must be an owner of the crate on crates.io
  • the repository must live on GitHub or GitLab

If the crate does not exist yet, bootstrap it first with a real initial release or monochange step placeholder-publish. The first publish still uses the normal crates.io token flow.

UI path

  • crate page → Settings → Trusted Publishing

Fields to enter for GitHub Actions

  • Repository owner: GitHub owner
  • Repository name: GitHub repository name
  • Workflow filename: for example release.yml
  • Environment: optional, for example release

Use the workflow filename only, not the full .github/workflows/... path.

Workflow setup

A typical GitHub Actions release job looks like this:

name: Publish to crates.io

on:
  push:
    tags:
      - "v*"

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: rust-lang/crates-io-auth-action@v1
        id: auth
      - run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

If you configure an environment on crates.io, the GitHub job must use the same environment name.

monochange notes

  • monochange does not create the crates.io trusted-publisher record for you yet.
  • monochange preflights the GitHub repository/workflow/environment context it expects for manual registries and reports when one of those values still needs to be set explicitly in config.
  • Once the registry-side configuration exists, monochange can publish with the temporary token exposed by rust-lang/crates-io-auth-action@v1.
  • crates.io issues a short-lived publish token; the current docs describe these tokens as expiring after 30 minutes.
  • Use a specific workflow filename and, when needed, a protected GitHub environment to reduce the publish attack surface.
  • The current monochange GitHub publish workflow already uses this pattern.

Useful references:

  • https://crates.io/docs/trusted-publishing
  • https://rust-lang.github.io/rfcs/3691-trusted-publishing-cratesio.html

jsr

Registry-side setup

JSR supports tokenless publishing from GitHub Actions.

Manual setup step

  • go to the package on jsr.io
  • open Settings
  • link the package to the GitHub repository that is allowed to publish it

monochange reports the package URL and expects this repository-linking step to be completed manually.

Workflow setup

A minimal GitHub Actions job looks like this:

permissions:
  contents: read
  id-token: write

steps:
  - uses: actions/checkout@v6
  - run: npx jsr publish

You can also publish with:

deno publish

monochange notes

  • JSR’s tokenless publishing is GitHub Actions focused.
  • Other CI providers still need token-based publishing.
  • monochange does not yet link the package to the repository for you.
  • If the package does not exist yet, placeholder publishing can bootstrap the registry entry before you finish the repository-link step.

Useful references:

  • https://jsr.io/docs/publishing-packages
  • https://jsr.io/docs/trust

pub.dev

Registry-side setup

pub.dev calls this automated publishing.

Automated publishing on pub.dev authenticates with a temporary GitHub-signed OIDC token instead of a long-lived pub credential.

Prerequisites

  • the package must already exist on pub.dev
  • you must be an uploader or admin for the package
  • the repository must be on GitHub

If the package does not exist yet, publish it once first or use monochange step placeholder-publish.

UI path

  • https://pub.dev/packages/<package>/admin
  • find the Automated publishing section
  • click Enable publishing from GitHub Actions

Fields to enter

  • Repository: owner/repo
  • Tag pattern: a string containing {{version}}

Examples:

  • single-package repo: v{{version}}
  • monorepo package-specific tag: my_package-v{{version}}

For a monorepo, give each package its own tag pattern so a tag for one package cannot publish another package by accident. The official pub.dev guidance also recommends a separate workflow file per package when one repository publishes multiple Dart packages. For a broader monorepo strategy across registries, see Multi-package publishing patterns.

Optional hardening

  • click Require GitHub Actions environment on the package admin page
  • choose an environment name such as pub.dev
  • use the same environment name in the GitHub workflow

Workflow requirements

pub.dev only accepts GitHub Actions automated publishing when the workflow was triggered by a tag push. It rejects branch-triggered and manually dispatched workflows for this publishing flow.

That means the GitHub workflow trigger must align exactly with the configured tag pattern.

pub.dev strongly encourages the reusable workflow maintained by dart-lang/setup-dart:

name: Publish to pub.dev

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"

jobs:
  publish:
    permissions:
      id-token: write
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    # with:
    #   working-directory: path/to/package/within/repository

If you require a GitHub Actions environment on pub.dev, pass the same environment name to the reusable workflow:

jobs:
  publish:
    permissions:
      id-token: write
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    with:
      environment: pub.dev
      # working-directory: path/to/package/within/repository

Custom workflow

If you need custom code generation or build steps, set up Dart yourself and publish manually after the OIDC-authenticated setup step:

dart pub publish --force

For Flutter packages, the equivalent publish command is:

flutter pub publish --force

monochange notes

  • monochange does not configure pub.dev automated publishing for you yet.
  • monochange reports the package admin URL so you can finish the setup manually.
  • pub.dev is stricter than the others here because the workflow must be tag-triggered, not just manually dispatched or branch-triggered.
  • The reusable workflow from dart-lang/setup-dart is the officially recommended path and is worth preferring unless you need custom pre-publish steps.
  • Keep the Git tag, pubspec.yaml version, and tag pattern aligned.
  • Protect matching tags, and use GitHub environment protection rules when you need an approval gate before publishing.

Useful references:

  • https://dart.dev/tools/pub/automated-publishing
  • https://pub.dev/packages/<package>/admin

Mapping monochange config to registry values

Use this cheat sheet when a registry asks for workflow details.

Registry fieldValue to use
repository owner / organization / namespaceGitHub owner from [source] or publish.trusted_publishing.repository
repository name / projectrepository part of owner/repo
workflow filenamepublish.trusted_publishing.workflow, for example publish.yml
environmentpublish.trusted_publishing.environment, for example publisher
pub.dev tag patternchoose a tag rule that matches your release workflow, for example v{{version}} or my_package-v{{version}}

If monochange cannot infer the GitHub repository or workflow for a package, set them explicitly in monochange.toml.

Security recommendations

  • prefer trusted publishing over long-lived registry tokens whenever the registry supports it
  • keep id-token: write only on the publish job instead of the entire workflow when possible
  • use a protected GitHub environment such as publisher for high-value publish jobs
  • restrict tag creation and release workflows to trusted maintainers
  • use package-specific tags in monorepos when a registry supports tag-based publish authorization

When to keep mode = "external"

Keep a package on mode = "external" when:

  • the registry is private or custom
  • you need retry or delayed requeue behavior that monochange does not manage yet
  • your registry requires a CI pattern that differs substantially from monochange’s built-in publish flow

In those cases, you can still use the same registry-side trusted-publishing setup while letting your own workflow own the actual publish command. The same approach is often the cleanest fit for multi-package repositories that need package-specific tags or workflows; see Multi-package publishing patterns.

Possible future automation for manual registries

monochange is intentionally conservative here.

Today, monochange does not perform registry-side trusted-publishing enrollment during real package publishing. npm has the richest repair guidance because monochange can render the expected npm trust github ... command, but the command is still manual or external. For crates.io, jsr, pub.dev, and PyPI, monochange focuses on setup guidance, preflight checks, and actionable diagnostics instead of trying to mutate registry-side trust records automatically.

Areas that may become more automated later, where the registry and CI contracts make it safe enough, include:

  • crates.io: stronger preflight validation around explicit workflow filenames, environment alignment, and clearer checks for first-publish bootstrap versus post-bootstrap trusted publishing
  • jsr: better repository-link diagnostics and package metadata checks before publish, especially when the package already exists but repository-side linking is incomplete
  • pub.dev: stronger validation that tag patterns, workflow triggers, working directories, and optional environments still match the automated-publishing setup expected by pub.dev
  • PyPI: stronger validation that the project trusted-publisher settings match the workflow name, environment, and package path monochange expects before running uv publish

Areas that monochange does not promise:

  • auto-enrolling registry-side trusted-publisher records for any registry, including npm, crates.io, jsr, pub.dev, or PyPI
  • bypassing browser-confirmed or admin-page-only steps that the registry intentionally keeps manual
  • inferring enough registry-side state to claim a package is fully enrolled when the registry does not expose that state safely or consistently

Treat this as a direction of travel, not a guarantee of upcoming behavior. If you need a registry-native workflow, keep the package on mode = "external" and let the registry-maintained workflow own the actual publish command.