| |

Dart 89 🎯 Publishing Open-Source Dart Packages to pub.dev with Automated Workflows

⏱️ Estimated read: 14 min | |

Publishing a Dart package to pub.dev is a one-way operation: once a version is uploaded, it cannot be deleted or overwritten. The manual workflow is a sequence of validation steps that catch problems before the irreversible upload, and the automated workflow replaces the manual confirmation with an OpenID Connect token exchange that GitHub Actions can perform without a long-lived secret. The Dart documentation describes both paths: dart pub publish --dry-run validates the package and lists the files it intends to upload, while the automated publishing setup on pub.dev allows a GitHub Actions workflow to publish on a tag push using a temporary OIDC token.

The manual workflow is the prerequisite for the automated one. A package that has never been published cannot be configured for automated publishing. The first version must be uploaded manually, which establishes ownership of the package name and creates the admin page where the automated publishing trust relationship is configured. The manual workflow also produces the README.md, CHANGELOG.md, LICENSE, and pubspec.yaml metadata that pub.dev requires, and the dry run is the check that those files are present and correctly formatted.

This chapter covers the package preparation checklist, the dry-run validation and the file list it produces, the manual publish confirmation and its irreversibility, the OIDC-based automated publishing setup for GitHub Actions, and the tag-driven workflow that publishes a release when a version tag is pushed. It assumes familiarity with pubspec.yaml and GitHub Actions.

Key point: dart pub publish --dry-run is the gate. It validates the package, lists every file that will be uploaded, and reports warnings. The actual publish is irreversible. The automated workflow replaces the manual confirmation with an OIDC token exchange, but it still runs the same validation. A tag push triggers the workflow, and the version in the tag must match the version in pubspec.yaml.


Why the publish workflow has a manual and an automated path

The first publish establishes ownership. pub.dev associates a package name with the account that first publishes it. The manual dart pub publish command authenticates the user interactively, confirms the package contents, and uploads. Only after that can the package’s admin page be configured for automated publishing. A workflow that attempts to publish a package that has never been manually published will fail because the package name is not associated with a publisher that the workflow is authorized to act for.

The dry run is the validation gate. dart pub publish --dry-run performs the same validation as the actual publish but stops before uploading. It verifies the pubspec.yaml format, the package layout conventions, and the presence of required files. It prints the list of files it intends to publish, and it reports the warning count. The Dart documentation recommends examining the file list and cancelling if any undesired files appear, because the package archive includes everything not excluded by .pubignore or .gitignore.

The upload is irreversible. Once a version is published, it cannot be deleted or modified. A new version is the only way to correct a mistake. This is why the dry run is not optional in practice: the cost of discovering a missing CHANGELOG.md or an accidentally included .env file after upload is a version bump and a new release. The --force flag skips the confirmation prompt, but it does not skip the validation. If the package has warnings and --force is used, the upload proceeds; without --force, the interactive prompt shows the warnings and asks for confirmation.

OIDC removes the long-lived secret. The automated publishing setup uses GitHub Actions’ OpenID Connect provider to issue a short-lived token that pub.dev trusts. The workflow requests a token with the id-token: write permission, and pub.dev validates the token’s claims against the configured repository, environment, and tag pattern. No PUB_DEV_TOKEN secret is stored in the repository. The alternative, a long-lived upload token in a repository secret, works but is less secure: the token can be used from anywhere, while the OIDC token is scoped to a specific workflow run and expires quickly.

The trade-off. The manual path is slower and requires a developer to be present for the confirmation. The automated path requires an initial manual publish and a one-time configuration on pub.dev. The automated path also requires the tag pattern to match the version, and a mismatch produces a publish failure. The manual path has no such constraint. For a package that releases infrequently, the manual path is sufficient. For a package with a regular release cadence, the automated path removes the human from the loop and makes the release a tag push.


a. The pre-publish checklist and the dry run

The package must have a pubspec.yaml with a name, version, description, and environment constraint. The description should be 60 to 180 characters and tell a casual reader what the package does. The homepage or repository field should point to the source, and the issue_tracker should point to the issue tracker. A README.md, CHANGELOG.md, and LICENSE are required for a complete package.

The code should pass the quality gates before the dry run. dart analyze should report no issues, and dart test should pass. The formatter should be applied. The dry run does not run the tests, but a package that fails the tests should not be published regardless.

The dry run command is dart pub publish --dry-run. It prints the package name and version, the list of files, and the warning count. A clean dry run shows Package has 0 warnings. The file list is the critical output: it shows every file that will be uploaded, and any file that should not be public—a .env, a build artifact, a test fixture with credentials—must be removed or added to .pubignore before the actual publish.

# Run the validation without uploading.
dart pub publish --dry-run

# Output example:
# Publishing transmogrify 1.0.0
# ├── README.md
# ├── lib/
# │   ├── transmogrify.dart
# │   └── src/
# ├── pubspec.yaml
# └── test/
# Package has 0 warnings.

The dry run does not check that the git status is clean. The actual publish does: it warns if tracked files have uncommitted changes. A clean working tree is a prerequisite for a reproducible release, and the warning is a signal that the published package may not match the tagged commit.


b. The manual publish and the confirmation prompt

The manual publish command is dart pub publish without --dry-run. It performs the validation, lists the files, and asks for confirmation before uploading. The prompt shows the package name and version, and the user types y to proceed. If the package has warnings, the prompt shows them, and the user must still confirm. If the package has errors, the command exits without uploading.

# Publish interactively.
dart pub publish

# Prompt:
# Do you want to publish transmogrify 1.0.0 (y/n)?

The --force flag skips the confirmation prompt. It is intended for CI environments where there is no stdin. In a manual workflow, it removes the opportunity to review the file list and the warnings, which is the last chance to catch a problem before the irreversible upload. The Dart documentation recommends not using --force without a prior dry run.

The --skip-validation flag publishes without the client-side validation or dependency resolution. It is for advanced cases, such as publishing two dependent packages where the second depends on a version of the first that is not yet available on pub.dev. The flag bypasses the check that the dependency exists, which is the only way to publish the second package before the first has propagated. It should not be used to bypass a validation failure that the developer does not understand.


c. Automated publishing with OIDC and GitHub Actions

The automated publishing setup has two parts: the configuration on pub.dev and the workflow in the repository. On pub.dev, the package’s admin page has an “Automated publishing” section. The publisher selects GitHub Actions as the provider, specifies the repository, and optionally requires a GitHub Actions environment. The environment requirement means the workflow must declare environment: pub.dev and the environment must exist in the GitHub repository settings.

The workflow uses dart-lang/setup-dart/.github/workflows/publish.yml@v1, which handles the OIDC token exchange and the publish command. The workflow must request id-token: write permission for the token to be issued. The trigger is typically a tag push matching a version pattern, such as v[0-9]+.[0-9]+.[0-9]+.

name: Publish to pub.dev

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'  # v1.2.3

jobs:
  publish:
    permissions:
      id-token: write   # Required for OIDC
      contents: read
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    with:
      environment: pub.dev

The tag pattern is a claim that the workflow validates against the token. The version in the tag must match the version in pubspec.yaml. If the tag is v1.2.3 and the pubspec.yaml says 1.2.2, the publish fails because the version is already published or the tag does not match. The tag is the release marker, and the version in the manifest is the source of truth.

A known issue with the OIDC flow is that Flutter-bundled dart may not auto-detect the GitHub Actions OIDC environment and fall back to interactive OAuth, which hangs in CI because there is no stdin. The workaround is to mint the OIDC token explicitly with curl and register it with dart pub token add https://pub.dev before running dart pub publish --force. The flutter-actions/setup-pubdev-credentials action handles this exchange for Flutter projects.


Complete Example Session

# ============================================
# PART 1: PUBSPEC.YAML METADATA
# ============================================
name: my_package
version: 1.2.3
description: A short description of what the package does.
homepage: https://github.com/user/my_package
repository: https://github.com/user/my_package
issue_tracker: https://github.com/user/my_package/issues

environment:
  sdk: ^3.9.0

# ============================================
# PART 2: REQUIRED FILES
# ============================================
# README.md
# CHANGELOG.md
# LICENSE

# ============================================
# PART 3: PRE-PUBLISH CHECKS
# ============================================
dart analyze
dart test
dart format --output=none --set-exit-if-changed .

# ============================================
# PART 4: DRY RUN
# ============================================
dart pub publish --dry-run

# Review the file list.
# Fix any warnings.
# Ensure 0 warnings.

# ============================================
# PART 5: MANUAL PUBLISH (FIRST TIME)
# ============================================
dart pub publish

# Confirm the prompt.
# Upload is irreversible.

# ============================================
# PART 6: CONFIGURE AUTOMATED PUBLISHING
# ============================================
# On pub.dev: Admin → Automated publishing
# Select GitHub Actions.
# Repository: user/my_package
# Environment: pub.dev (optional)
# Tag pattern: v{{version}}

# ============================================
# PART 7: GITHUB ACTIONS WORKFLOW
# ============================================
name: Publish

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

jobs:
  publish:
    permissions:
      id-token: write
      contents: read
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    with:
      environment: pub.dev

# ============================================
# PART 8: TAG AND RELEASE
# ============================================
# Update pubspec.yaml version to 1.2.3.
# Update CHANGELOG.md.
# Commit.
git tag v1.2.3
git push origin v1.2.3

# Workflow triggers, validates, publishes.

# ============================================
# PART 9: OIDC TROUBLESHOOTING
# ============================================
# If dart pub publish hangs in CI:
# Mint OIDC token explicitly:
# OIDC_TOKEN=$(curl ... audience=https://pub.dev)
# echo "$OIDC_TOKEN" | dart pub token add https://pub.dev
# dart pub publish --force

# ============================================
# PART 10: THE COMPLETE FLOW
# ============================================
# 1. Prepare pubspec, README, CHANGELOG, LICENSE.
# 2. Analyze, test, format.
# 3. Dry run, review files, fix warnings.
# 4. Manual publish for first version.
# 5. Configure automated publishing on pub.dev.
# 6. Add workflow with id-token: write.
# 7. Tag push triggers publish.

Quick Reference

Publish Commands

CommandEffect
dart pub publish --dry-runValidate, list files, no upload
dart pub publishInteractive confirm, upload
dart pub publish --forceSkip confirm, upload (CI)
dart pub publish --skip-validationBypass client validation (advanced)

Required Files

FilePurpose
pubspec.yamlName, version, description, environment
README.mdPackage documentation
CHANGELOG.mdVersion history
LICENSELicense text

Automated Publishing Setup

StepLocation
Configure trustpub.dev/packages/<name>/admin
ProviderGitHub Actions
Repositoryuser/repo
Environmentpub.dev (optional, recommended)
Tag patternv{{version}}

Workflow Permissions

PermissionPurpose
id-token: writeOIDC token exchange
contents: readCheckout repository

Best Practices

✅ Do This:

# Use dry run before every publish.                          // ✅
dart pub publish --dry-run

# Review the file list from the dry run.                     // ✅
# Remove or .pubignore unwanted files.

# Ensure 0 warnings before publishing.                       // ✅
# Fix README, CHANGELOG, metadata issues.

# Use OIDC for automated publishing.                         // ✅
# No long-lived secret; scoped to workflow run.

# Match tag to pubspec version.                              // ✅
git tag v1.2.3  # pubspec: version: 1.2.3

❌ Don’t Do This:

# Publish with warnings using --force.                       // ❌
dart pub publish --force  # Uploads with warnings.

# Include .env or credentials in the package.                // ❌
# Dry run file list reveals them; remove before publish.

# Skip the dry run on the first publish.                     // ❌
# Upload is irreversible; the file list is the last check.

# Use a long-lived PUB_DEV_TOKEN secret.                     // ❌
# Prefer OIDC for automated publishing.

Common Pitfalls

PitfallWhy It HappensFix
Dry run shows unexpected files.pubignore missing or incorrectAdd exclusions, re-run dry run
Publish fails on warnings--force skipped validation promptFix warnings; dry run first
Automated publish hangsFlutter dart misses OIDC environmentMint token explicitly with curl
Tag version mismatchTag v1.2.3, pubspec 1.2.2Align version before tagging
Cannot configure automationPackage never manually publishedPublish first version manually

Real-World Examples

1. Dry Run

dart pub publish --dry-run

2. Manual Publish

dart pub publish

3. Force Publish (CI)

dart pub publish --force

4. Skip Validation

dart pub publish --skip-validation

5. OIDC Workflow

permissions:
  id-token: write
uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1

6. Tag Pattern

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

7. Pubspec Version

version: 1.2.3

8. Tag Command

git tag v1.2.3
git push origin v1.2.3

9. Explicit OIDC Token

echo "$OIDC_TOKEN" | dart pub token add https://pub.dev

10. Flutter Credentials Action

uses: flutter-actions/setup-pubdev-credentials@v1

Visual

Publish Pipeline

┌──────────────────────────────────────────────┐
│  PRE-PUBLISH                                 │
│                                              │
│  pubspec.yaml                                │
│  README.md, CHANGELOG.md, LICENSE            │
│       │                                      │
│       ▼                                      │
│  dart analyze                                │
│  dart test                                   │
│  dart format --set-exit-if-changed           │
│       │                                      │
│       ▼                                      │
│  dart pub publish --dry-run                  │
│    → validate pubspec                        │
│    → list files                              │
│    → report warnings                         │
│       │                                      │
│       ├── Warnings → fix, re-run             │
│       │                                      │
│       ▼                                      │
│  0 warnings                                  │
│                                              │
└──────────────────────────────────────────────┘

Manual vs Automated

┌──────────────────────────────────────────────┐
│  MANUAL                AUTOMATED             │
│                                              │
│  dart pub publish      Tag push              │
│  Interactive confirm   OIDC token            │
│  First publish         Subsequent releases   │
│                                              │
│  Both:                                       │
│  - Validate package                          │
│  - Upload to pub.dev                         │
│  - Irreversible                              │
│                                              │
└──────────────────────────────────────────────┘

OIDC Flow

┌──────────────────────────────────────────────┐
│  GITHUB ACTIONS                              │
│                                              │
│  Tag push (v1.2.3)                           │
│       │                                      │
│       ▼                                      │
│  Workflow starts                             │
│    id-token: write                           │
│       │                                      │
│       ▼                                      │
│  setup-dart/publish.yml@v1                   │
│    → request OIDC token                      │
│    → audience: pub.dev                       │
│       │                                      │
│       ▼                                      │
│  pub.dev validates token                     │
│    → repository matches                      │
│    → environment matches                     │
│    → tag pattern matches                     │
│       │                                      │
│       ▼                                      │
│  dart pub publish --force                    │
│       │                                      │
│       ▼                                      │
│  Published.                                  │
│                                              │
└──────────────────────────────────────────────┘

Dry Run Output

┌──────────────────────────────────────────────┐
│  dart pub publish --dry-run                  │
│                                              │
│  Publishing my_package 1.2.3                 │
│  ├── README.md                               │
│  ├── CHANGELOG.md                            │
│  ├── LICENSE                                 │
│  ├── lib/                                    │
│  │   ├── my_package.dart                     │
│  │   └── src/                                │
│  ├── pubspec.yaml                            │
│  └── test/                                   │
│                                              │
│  Package has 0 warnings.                     │
│                                              │
│  Review file list before publishing.         │
│  Upload is irreversible.                     │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Dry rundart pub publish --dry-run
Manual publishdart pub publish (interactive confirm)
Force publishdart pub publish --force (CI)
Required filespubspec.yaml, README.md, CHANGELOG.md, LICENSE
Automation setuppub.dev admin → GitHub Actions
OIDC permissionid-token: write
Workflow actiondart-lang/setup-dart/.github/workflows/publish.yml@v1
Tag patternv[0-9]+.[0-9]+.[0-9]+
Tag/version matchRequired for automated publish
IrreversibilityPublished versions cannot be deleted

Key takeaways:

  • The dry run is the validation gate. dart pub publish --dry-run validates the package, lists every file that will be uploaded, and reports warnings. The actual publish is irreversible, so the file list is the last chance to catch an unwanted file.
  • The first publish must be manual. pub.dev associates the package name with the first publisher, and the automated publishing configuration is only available after that. A workflow cannot publish a package that has never been manually published.
  • OIDC removes the long-lived secret. The automated workflow requests a short-lived token with id-token: write, and pub.dev validates the token’s repository, environment, and tag claims. The token is scoped to the workflow run and expires, unlike a PUB_DEV_TOKEN secret.
  • The tag must match the pubspec.yaml version. A tag v1.2.3 requires version: 1.2.3 in the manifest. A mismatch fails the publish because the version is already published or the tag does not correspond to the release.
  • The environment requirement adds approval gates. pub.dev can require a GitHub Actions environment, and GitHub can require reviewers for that environment. The environment is reflected in the OIDC token, so a user with repository write access cannot bypass the approval by editing the workflow.
  • Flutter-bundled dart may need an explicit token exchange. If the publish hangs in CI waiting for interactive OAuth, the workaround is to mint the OIDC token with curl and register it with dart pub token add https://pub.dev before publishing.

Remember: Publishing to pub.dev is a one-way operation. The dry run is the only chance to see the file list before it becomes public, and the warning count is the signal that the package metadata is incomplete. The manual publish establishes ownership; the automated publish removes the human from the release loop but requires a one-time trust configuration and a tag that matches the manifest version. The OIDC token is scoped to the workflow run and the environment, which is why it is safer than a long-lived secret. Use the dry run, fix the warnings, publish the first version manually, configure the trust, and let the tag push handle the rest.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!