Package Publishing to pub.dev

This guide covers the workflow for publishing packages to pub.dev and managing version updates across the workspace.

Published Packages

This workflow applies to every workspace package that exists on pub.dev — 30 of them on 2026-09-15, all under tom_ai/.

The list is not written down here, deliberately. It was, and it rotted: the table named eight packages and omitted fifteen, including tom_d4rt_ast and tom_d4rt_exec, which publish several times a week. A table missing the package you are about to publish reads as "this procedure does not apply to me", which is the opposite of what it is for.

Ask pub.dev instead. It is the authority, it takes a few seconds, and it cannot go stale:

grep -rl --include=pubspec.yaml '^name: tom_' tom_ai |
  xargs grep -h '^name:' | awk '{print $2}' | sort -u |
  while read -r p; do
    code=$(curl -s -o /dev/null -w '%{http_code}' "https://pub.dev/api/packages/$p")
    [ "$code" = 200 ] && echo "$p"
  done

Two cheaper checks that are NOT equivalent, and knowing why saves a wrong answer:

  • Absence of publish_to: none means publishABLE, not published. It

matches 58 directories here, most of them test fixtures and vendored forks. - A directory in ~/.pub-cache/hosted/pub.dev/ proves a package was published AND resolved on THIS machine, so it is a floor rather than the set — it reported 23, missing tom_d4rt_flutter_ast, tom_dcli_exec and tom_code_specs, which are published and simply not resolved here.

CRITICAL: tom_build_base Dependency Rule

All tools and projects that depend on tom_build_base MUST use pub.dev dependencies (not path dependencies).

Why This Matters

  • tom_build_base is the foundation library for all build tools (buildkit, testkit, issuekit, etc.)
  • Changes to tom_build_base require publishing to pub.dev before dependent tools can use them
  • Path dependencies bypass version resolution and can cause untested code to be deployed

Projects That Must Use pub.dev tom_build_base

The rule, not a list: every project that depends on tom_build_base must depend on it from pub.dev. The curated list that used to sit here rotted — it named five projects, one of them _build, which has never been a workspace directory at all. Derive the list instead, from the workspace root:

# Who depends on it (22 pubspecs on 2026-09-08)
grep -rl --include=pubspec.yaml 'tom_build_base' \
    tom_ai tom_forge tom_assistant tom_sqm tom_uam tom_worktracker \
  | grep -v '/build/\|\.dart_tool\|/tom_build_base/' | sort

# Who breaks the rule with a path: dependency (6 on the same date)
grep -rn -A1 --include=pubspec.yaml 'tom_build_base:' \
    tom_ai tom_forge tom_assistant tom_sqm tom_uam tom_worktracker \
  | grep -v '/build/\|\.dart_tool' | grep 'path:'

The second command found six on 2026-09-08: the three tom_basics_samples/* packages, tom_package_scanner, tom_dart_editor_bundler and tom_website_editor_core. Whether a sample sitting beside tom_build_base is a deliberate exception is a question for the owning quests; the rule above is not in doubt, and the command is how you check it rather than trusting a list.

Update Workflow for tom_build_base Changes

When making changes to tom_build_base:

1. Make changes in tom_build_base 2. Run tests in tom_build_base: dart test 3. Update version in tom_build_base pubspec.yaml 4. Publish tom_build_base: dart pub publish 5. Update dependent projects:

   cd tom_ai/basics/tom_build_kit
   dart pub upgrade tom_build_base
   
   cd tom_ai/devops/tom_test_kit
   dart pub upgrade tom_build_base
   
   # etc. for all dependent projects

6. Run tests in dependent projects to verify compatibility 7. Recompile tools if they have compiled binaries

pubspec.yaml Example (Correct)

dependencies:
  tom_build_base: ^1.8.0    # ✅ pub.dev version constraint

pubspec.yaml Example (WRONG - Do Not Use)

dependencies:
  tom_build_base:
    path: ../tom_build_base  # ❌ Path dependency - changes bypass publishing

Exception: Path dependencies are ONLY acceptable during active development within a single session, and must be reverted before committing.

Publishing Does Not Reach Existing Consumers by Itself

A lower-bound-only constraint is not a mechanism for propagating a publish — it is a mechanism for never propagating one. Step 5 of the workflow above is not housekeeping; skipping it leaves every consumer measuring the previous release.

dart pub get is lock-preserving. When pubspec.lock already holds a version that still satisfies the constraint, pub get keeps that version no matter how far behind the constraint's upper reach it sits. A constraint of >=1.4.0 admits 1.20.0 but never selects it once a lock exists. Only one of these moves it:

  • dart pub upgrade <package> (or flutter pub upgrade in a Flutter package),
  • deleting pubspec.lock, or
  • raising the lower bound above the locked version.

So after publishing a tom_* package that consumers must actually see, do one of those in every consumer, and say in the commit or todo which of the two you did. "The constraint admits it" is not an answer.

Why the drift is invisible

pubspec.lock is gitignored across this workspace. The resolved version is therefore pure per-machine state — never reviewed, never diffed, never synced across the fleet machines — and no test output names it. A suite can run green for months against a package version nobody has checked.

Do not respond to this by checking pubspec.lock into git. That trades an invisible per-machine drift for merge conflicts on a generated file across four machines, which is why the workspace has a generated-file merge policy at all. What is missing is a check that the resolution matches the declaration, not a versioned lock.

Nested packages carry their own locks

A top-level pub upgrade reaches the package it runs in and nothing else. Every directory with its own pubspec.yaml — example/*/ fixtures, companion apps nested under test/, sample apps — is a separate package with a separate gitignored lock, and a repository can have more of them than any checklist lists. Upgrade each one, or the binaries those fixtures compile will resolve a different version than the package under test.

A worked instance of both failure modes, with the guard that detects them, is tom_ai/d4rt/tom_d4rt_ast/test/scc45_resolution_guard_test.dart.

First-Time Publishing

When converting a project from internal-only to pub.dev publishable for the first time:

1. Create LICENSE File

By default, create a LICENSE file with all rights reserved:

Copyright (c) 2024-2026 Alexis Kyaw. All rights reserved.

This code is proprietary and confidential. Unauthorized copying, modification,
distribution, or use of this software, via any medium, is strictly prohibited.

For licensing inquiries, contact Alexis Kyaw on LinkedIn.

Note: This is the default for internal packages. For packages intended for public use, choose an appropriate open-source license (MIT, BSD, Apache 2.0, etc.).

2. Update pubspec.yaml

Remove or change the publish_to field:

# Before (internal only)
publish_to: none

# After (for pub.dev)
# Remove the line entirely, or:
# publish_to: https://pub.dev  # (optional, this is the default)

Add required fields:

name: tom_my_package
description: A meaningful one-line description (min 60 chars recommended)
version: 1.0.0
homepage: https://github.com/al-the-bear/tom
repository: https://github.com/al-the-bear/tom

environment:
  sdk: ^3.5.0

# Convert path dependencies to version constraints
dependencies:
  tom_basics: ^1.8.0  # Not path: ../tom_basics

3. Prepare README.md

pub.dev requires a README.md with:

  • Package description: What does it do?
  • Installation: How to add to pubspec.yaml
  • Basic usage: Simple code example
  • API overview: Key classes/functions

Minimum template:

# Package Name

Brief description of what the package does.

## Installation

Add to your `pubspec.yaml`:

\`\`\`yaml
dependencies:
  tom_my_package: ^1.0.0
\`\`\`

## Usage

\`\`\`dart
import 'package:tom_my_package/tom_my_package.dart';

// Basic usage example
\`\`\`

## Features

- Feature 1
- Feature 2

4. Set Up Library Exports

Ensure lib/<package_name>.dart exports the public API:

// lib/tom_my_package.dart
library tom_my_package;

export 'src/public_class.dart';
export 'src/public_functions.dart';
// Do NOT export internal implementation files

5. Run Pre-Publish Checks

# Verify exports work
dart analyze

# Check pub.dev requirements
dart pub publish --dry-run

Address all warnings before proceeding to publish.

6. First Publish

dart pub publish

Package Dependency Chain

Publishing order matters. Changes to lower-level packages may require republishing dependent packages.

tom_basics            tom_crypto
     ↓                     ↓
tom_build_base        tom_reflection
     ↓                     ↓
tom_d4rt          tom_reflection_generator
     ↓
tom_d4rt_generator

Use buildkit -R :dependencies --deep to see the complete dependency tree.

Before Publishing

1. Check Current pub.dev Version

dart pub outdated
# or
dart pub deps --outdated

2. Review Git Changes

git log --oneline origin/main..HEAD
git diff origin/main

3. Run Quality Checks

dart analyze
dart test
dart format -o none --set-exit-if-changed .

The -o none is load-bearing, not tidiness. --set-exit-if-changed sets the EXIT CODE; it does not stop the formatter writing, because -o defaults to write. Without -o none the command you reached for to ASK whether anything was unformatted reformats everything that was — and it does it silently, since the only output is the exit code you were looking for. On 2026-08-12 that rewrote 368 files in the middle of a release preparation.

Reach for it from memory and you will drop the flag, which is why the reason is written here rather than left as a convention.

4. Dry Run Publish

dart pub publish --dry-run

Address any warnings before proceeding.

5. Interpreters only: the pre-publish pass

Applies to tom_d4rt and tom_d4rt_ast, and to nothing else. A generic "path-resolve everything before every publish" rule would be ignored within a month; these two earn it because their publishes are irreversible AND invisible downstream until they land.

Every package that consumes them — tom_d4rt_exec, both Flutter twins, both companion apps — resolves them from pub.dev, deliberately, so that their suites certify what a consumer actually gets. The cost of that choice is that nothing downstream can fail on an interpreter change until after the release carrying it. The pass is the one hour in which that is not true:

cd tom_ai/d4rt/tom_d4rt_flutter_ast
dart run tool/prepublish_overrides.dart --pass

It path-resolves every consumer against the working tree, runs flutter analyze in both twins and dart analyze plus the full suite in tom_d4rt_exec, and restores hosted resolution whatever the outcome. A non-zero exit means do not publish.

gatecatchescost
analyze, both twins + exec an API break that stops a consumer compiling ~4 min
tom_d4rt_exec's suite behavioural drift neither interpreter tree can see — exec carries stdlib and conformance suites they do not ~3 min
the base corpus name-resolution blast radius, which is invisible to unit tests by construction ~1 h per twin, serial

The first two are non-negotiable and are what --pass runs. The corpus is required in addition when the release changes NAME RESOLUTION — the same rule the d4rt quest already applies to the POST-publish corpus run, moved to where it can still change the outcome. Drive both twins serially, never in parallel.

A pre-publish run is NOT recorded under ## Verification runs. That section records which PUBLISHED pair was measured, and an entry naming a version nobody can install is worse than no entry: the pass decides whether to publish, and the recorded run happens afterwards.

This is a written procedure and its enforcement is that the next person reads it. Nothing on disk distinguishes "about to publish" from "not about to publish", so there is no test to write — which is exactly why the work was put behind one command instead of three.

Publishing Workflow

Step 1: Update Version

In pubspec.yaml, follow semantic versioning:

  • MAJOR (x.0.0): Breaking API changes
  • MINOR (0.x.0): New features, backward compatible
  • PATCH (0.0.x): Bug fixes, backward compatible
# pubspec.yaml
version: 1.2.3  # Increment appropriately

Step 2: Update CHANGELOG.md

Document changes at the top of CHANGELOG.md:

## 1.2.3

- Fixed issue with X
- Added support for Y
- Breaking: Renamed `oldMethod` to `newMethod`

Step 3: Update README.md

If API or usage changed, update documentation: - Update code examples - Document new features - Update any version badges

Step 4: Commit Changes

git add pubspec.yaml CHANGELOG.md README.md
git commit -m "Prepare v1.2.3 for pub.dev release"

Step 5: Publish

dart pub publish

Review the package details and confirm when prompted.

Step 6: Tag Release

git tag -a tom_<package>-v1.2.3 -m "Release tom_<package> v1.2.3"
git push origin tom_<package>-v1.2.3

Step 7: Update Dependent Projects

For projects that depend on the published package:

1. Update version constraint in pubspec.yaml:

   dependencies:
     tom_build_base: ^1.2.3

2. Run dart pub get

3. Verify with dart analyze and dart test

Handling Path vs Version Dependencies

During development, we use path dependencies for tight integration:

# pubspec.yaml during development
dependencies:
  tom_build_base:
    path: ../tom_build_base

# or in pubspec_overrides.yaml
dependency_overrides:
  tom_build_base:
    path: ../tom_build_base

For publishing, packages must use version constraints:

# pubspec.yaml for publishing
dependencies:
  tom_build_base: ^1.8.0

Common Issues

"Published package depends on path dependency"

The published pubspec.yaml cannot have path dependencies. Use version constraints only.

"Version already exists"

You cannot overwrite a published version. Increment the version number.

"credentials not found"

Run dart pub login and authenticate with your pub.dev account.

A stale version listing after publishing — do NOT clean the cache

pub.dev's CDN can keep serving an old versions.json for several minutes after a successful publish, so dart pub upgrade in a consumer reports the new version as nonexistent. The fix is small and local:

rm ~/.pub-cache/hosted/pub.dev/.cache/<package>-versions.json
dart pub upgrade          # in the consumer

dart pub cache clean is the wrong tool and it is destructive far beyond the package you are publishing. The cache is shared by every Dart package on the machine, and cleaning it removes versions that OTHER checkouts still resolve. Measured when it was reached for during a release: dart pub cache clean -f removed tom_d4rt-1.53.0, tom_d4rt_ast-0.42.0, tom_d4rt_dcli-1.3.0 and tom_d4rt_flutter-1.2.0, and four unrelated packages in the tree then failed with

Error when reading '…/tom_d4rt_ast-0.42.0/lib/ast.dart': No such file or directory

which reads as a dependency bug and is a missing archive. It did not help with the stale listing either — that was the CDN, not the cache.

Why it cannot be diagnosed from the repository. pubspec.lock is gitignored across this workspace, so nothing committed records what each package had resolved. The damage is visible only in the working tree, on one machine.

The workspace already has the detector. _bin/check_package_configs.py walks every .dart_tool/package_config.json and names each project whose config points at a cache directory that is not there:

./_bin/check_package_configs.py              # exit 1 when a config names a missing root
./_bin/check_package_configs.py --summary    # counts by missing package
./_bin/check_package_configs.py some/tree    # limit the walk

Do not hand-roll the scan. If you do anyway, use find rather than grep -r: .dart_tool/ is gitignored, so a gitignore-respecting search — ripgrep, git grep, the wrapper several editors install over grep — reports nothing and reads as "the cache is intact".

Two repairs, and which one you need depends on why the entry is missing:

  • The cache lost a version something still resolves (what pub cache clean

does). dart pub get in the affected project re-downloads it, and is the remedy the script prints. dart pub cache add <package> --version <v> does the same for one entry without touching the project's resolution, which is the safer move mid-release when you do not want the lock to move. - The config is simply stale — it names a version this project no longer depends on. dart pub get rewrites it. Nothing was lost.

Breaking Changes in Dependencies

When a dependency makes breaking changes:

1. Check if your code is affected 2. Update your code if needed 3. Bump your MINOR or MAJOR version 4. Update CHANGELOG to note the dependency change

Automation Limitations

Currently, there is no automated tool support for: - Automatically updating version constraints in dependent packages - Full publishing cycle across multiple packages - Detecting which packages need republishing

These steps must be performed manually.

Workflow Checklist

Before publishing, verify:

  • [ ] All tests pass: dart test
  • [ ] No analyzer issues: dart analyze
  • [ ] Code formatted: dart format -o none --set-exit-if-changed . — with -o none, or it rewrites instead of reporting
  • [ ] Version bumped in pubspec.yaml
  • [ ] CHANGELOG.md updated
  • [ ] README.md updated (if needed)
  • [ ] Dry run passes: dart pub publish --dry-run
  • [ ] Interpreters only (tom_d4rt, tom_d4rt_ast): pre-publish pass clean — dart run tool/prepublish_overrides.dart --pass from tom_d4rt_flutter_ast, plus the base corpus if the release changes name resolution
  • [ ] Git commit with version changes
  • [ ] Dependent packages identified for update

After publishing:

  • [ ] Git tag created and pushed
  • [ ] Dependent packages updated with new version
  • [ ] dart pub get run in dependent packages
  • [ ] Dependent packages tested

See Also