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: nonemeans 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_baseis the foundation library for all build tools (buildkit, testkit, issuekit, etc.) -
Changes to
tom_build_baserequire 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>(orflutter pub upgradein 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.
| gate | catches | cost |
|---|---|---|
| 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 --passfromtom_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 getrun in dependent packages - [ ] Dependent packages tested
See Also
- Tool Dependencies - CLI tool rebuild instructions
- Projects and Repos - Project listing
- Dart Package Publishing - Official pub.dev guide