pub.dev
Detection signals
Section titled “Detection signals”pub.dev is present when a pubspec.yaml or a pubspec.lock turns up anywhere in the checkout. Dart requires name in every pubspec, published or not, so the manifest’s identity field settles nothing about publishing on its own.
pub.dev is shipped when the pubspec carries the fields Dart requires only of published packages and publish evidence exists: a release workflow running dart pub publish or flutter pub publish, or an existing page on pub.dev for that name. Dart documents version and description as mandatory “only when you intend to publish your package to the pub.dev site”, which makes their presence a useful signal and their absence a decisive one.
Sources: Pubspec format, Publishing packages.
Three cases decide most arguments:
publish_to: nonesettles the shipped answer. Dart’s own words: “Specifynoneto prevent a package from being published.” Almost every Flutter application carries it, which is why a repository full of Dart code often ships nothing through pub.dev.publish_tonaming a custom server means the package ships somewhere else. Report it as shipped and name the server, rather than reading a redirect as an absence.- A
pubspec.lockdecides nothing in either direction, because Dart tells a library package not to commit it and an application to commit it. Its absence from a library is the documented convention rather than a gap. Anexample/pubspec.yamlinside a published package is present and is not separately shipped.
Release track
Section titled “Release track”pub.dev takes the registry-push track. dart pub publish uploads a built archive to pub.dev under an authenticated account, so there is an upload to secure and a credential to scope, which is what assigns the track. The roster records "track": "registry-push" for pub.dev and the release area’s preamble names pub.dev in its registry-push list.
Verified 2026-07-31 against https://dart.dev/tools/pub/pubspec and https://dart.dev/tools/pub/publishing.
Version badge
Section titled “Version badge”Shields.io serves this one at img.shields.io/pub/v/PACKAGE, taking the package name as its only path parameter. ?include_prereleases widens it past stable releases.
The default label is pub. Keep it: pub is what the ecosystem calls its registry and its tool, and relabelling it dart or flutter claims a scope the badge does not check, since one package can serve both.
Link the badge to the package page:
[](https://pub.dev/packages/PACKAGE)Shields.io also serves pub/points, pub/likes, and pub/publisher for the same package. The pub points score is tempting because pub.dev computes it, and it still fails the live-fact test in badges.md: it measures the package against the registry’s own conventions rather than telling a reader whether the package works, and it is not the version, the build, or the coverage that the three slots are for.
Install command
Section titled “Install command”dart pub add PACKAGEDart documents that as adding the latest stable version compatible with the project’s SDK constraints, which means the README does not have to state a version at all and the command cannot suggest one the reader’s SDK rejects.
A package meant for Flutter shows flutter pub add PACKAGE, the same subcommand under the Flutter tool. Choose by what the package depends on rather than by preference: a package that imports package:flutter cannot be added to a plain Dart project, and showing the Dart form for it sends the reader into a resolution error.
Verified 2026-07-31 against dart pub add, Using packages, Pub Version badge, and services/pub/pub.service.js in badges/shields.
License declaration (R-COM-01)
Section titled “License declaration (R-COM-01)”pubspec.yaml has no license field. The pubspec reference names every supported top-level field, name, version, description, homepage, repository, issue_tracker, documentation, dependencies, dev_dependencies, dependency_overrides, environment, executables, platforms, publish_to, funding, false_secrets, screenshots, topics, ignored_advisories, and hooks, and none of them is a license. Pub ignores anything else in the file, so inventing a license key states nothing to anybody.
The license lives in a file instead, and Dart’s publishing guide requires one: include a LICENSE file in your package. It recommends the BSD 3-clause license, which the Dart and Flutter teams use, while allowing any license appropriate for the package.
That collapses R-COM-01 to one side, the same shape Go modules reaches by a different route. There is no manifest declaration to compare the file against, so what remains is the file existing at the repository root and containing the full text of a license you can name. Read it rather than trusting its filename, and where the repository also carries another ecosystem’s manifest, that manifest’s declaration is compared against the same file.
Source: The pubspec file and Publishing packages.
Funding platform name
Section titled “Funding platform name”Tidelift lists no platform for this ecosystem, so there is no tidelift value a Dart package can write.
GitHub’s own documentation for the funding file enumerates the accepted tidelift platform names. It names six, every one of them another ecosystem’s registry; Dart, Flutter, and pub.dev appear nowhere in it. That is why the roster at skills/oss-audit/ecosystems.json carries no tidelift key for this ecosystem.
Source: Displaying a sponsor button in your repository.
This is the one ecosystem with a documented fallback of its own. pubspec.yaml supports a top-level funding field, listed in the pubspec reference beside homepage and repository, so a Dart package states its funding links in its own manifest and pub.dev reads them from there. Use it in addition to the forge-level github, open_collective, liberapay, and custom keys, not instead of them: the manifest field reaches somebody browsing the registry and the funding file reaches somebody browsing the repository.
No rule in STANDARD.md requires a funding file at all, so this gap costs a repository nothing when it is scored, and the manifest field covers most of what the missing platform name would have. Tidelift adding a Dart platform, and GitHub’s funding documentation listing it, is what would retire it.
Verified 2026-07-31 against The pubspec file, Publishing packages, and Displaying a sponsor button in your repository.
Toolchain and matrix (R-CI-03)
Section titled “Toolchain and matrix (R-CI-03)”On GitHub Actions, dart-lang/setup-dart downloads a Dart SDK and adds dart to the PATH. Its sdk input takes a release channel, stable, beta, dev, or main, an SDK release version such as 3.1, or an exact version, so a matrix mixes pinned versions with channels:
strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] sdk: ['3.1', stable, beta]steps: - uses: dart-lang/setup-dart@v1 # oss-harden pins this to a commit SHA with: sdk: ${{ matrix.sdk }}The support claim lives in the environment field of pubspec.yaml, the sdk constraint, and it is not optional: Dart documents that omitting it is an error and dart pub get fails. So every package here makes a claim the matrix can be built from, which is not true of every ecosystem in this directory.
On GitLab, run the job in the Dart image and vary the tag with parallel:matrix.
Sources: dart-lang/setup-dart, The pubspec file, dart-lang/dart-docker.
Dependency caching (R-CI-04)
Section titled “Dependency caching (R-CI-04)”dart-lang/setup-dart has no cache input. Its README documents three inputs, sdk, flavor, and architecture, one output, dart-version, and nothing about dependencies; its own example runs dart pub get as an ordinary step.
Source: dart-lang/setup-dart.
The strongest documented fallback is an explicit cache over the pub system cache, which Dart does document: $HOME/.pub-cache on macOS and Linux, %LOCALAPPDATA%\Pub\Cache on Windows, and relocatable through PUB_CACHE. That is genuine download data, so the directory is not the problem. The key is.
Dart documents that an application package checks pubspec.lock into source control and a regular package, its word for a library, does not, because a library is expected to work across a range of dependency versions. A library therefore commits no lockfile, and a cache keyed on pubspec.yaml instead survives every resolution change the manifest’s ranges permit, which is what R-CI-04 is about. An application package that commits its lockfile has no gap: key on pubspec.lock. For a library, the honest options are to key on pubspec.yaml and accept that it caches across resolutions, or to run without a cache. Dart changing the library convention, or setup-dart gaining a keyed cache, would retire it.
On GitLab set PUB_CACHE to a project-relative directory first, because GitLab caches only paths inside the project directory.
Sources: Pub environment variables, Package layout conventions, GitLab CI/CD YAML reference, cache.
Test command (R-CI-06)
Section titled “Test command (R-CI-06)”dart test runs the tests, which live under the test directory of the package and depend on the test package being a dev dependency in pubspec.yaml. That dev dependency is the observable declaration: a package with no test entry under dev_dependencies has no suite dart test can run, whatever the directory holds.
setup-dart’s own README shows the full check sequence a Dart package runs, dart pub get, dart format --output=none --set-exit-if-changed ., dart analyze, then dart test, which is the order to keep in CI so the fast checks fail first.
Sources: dart test, dart-lang/setup-dart.
Verified 2026-07-31 against https://github.com/dart-lang/setup-dart, https://dart.dev/tools/pub/pubspec, https://dart.dev/tools/pub/environment-variables, https://dart.dev/tools/dart-test, https://github.com/dart-lang/dart-docker, and https://docs.gitlab.com/ci/yaml/.
Automated dependency updates (R-SEC-03)
Section titled “Automated dependency updates (R-SEC-03)”Dependabot’s pub value carries version updates and security updates, with private repositories and private hosted pub repositories both supported. It is a community-maintained ecosystem, listed as maintained by the Dart community rather than by GitHub, which is worth saying when reporting it: the coverage is real and its maintenance is not GitHub’s.
One documented behaviour changes what a quiet updater means here. Dependabot will not perform an update for pub when the version it tries to update to is ignored, even if an earlier version is available. An ignore rule on a newer release therefore suppresses the older update too, so read the ignore entries before concluding the updater has nothing to propose.
On GitLab the Renovate manager is pub.
Lockfile and frozen install (R-SEC-08)
Section titled “Lockfile and frozen install (R-SEC-08)”dart pub get writes pubspec.lock by default, so unlike NuGet there is no property to turn on, and the frozen install is dart pub get --enforce-lockfile.
What keeps the file out of some repositories is convention rather than tooling. Dart’s own documentation says application packages should check the lockfile into source control, and that regular packages, which is Dart’s word for a library, should not, since they are expected to work with a range of dependency versions. So this ecosystem is inside R-SEC-08 rather than outside it, and only the library case is what the rule’s commit-convention clause exempts. Establish which kind of package the repository holds before scoring the missing file, the same way NuGet’s library and application split is read.
What breaks the first time you commit the lockfile
Section titled “What breaks the first time you commit the lockfile”Generate the lock on the lowest SDK the matrix tests. Pub resolves against the installed toolchain: it “tries to find the latest version of a package whose SDK constraint works with the version of the Dart SDK that you have installed”. A lock written on the newest SDK can therefore name a dependency release whose own environment section excludes the oldest job, and that job fails at pub get rather than at test. Declaring the project’s own SDK constraint does not prevent it, because the constraint bounds the project and the resolver reads the running SDK.
Give the matrix a lowest-versions job as well. dart pub downgrade gets the lowest versions of every dependency, ignoring any existing lockfile and resolving the minimum that satisfies each constraint. That is the other end of the same range, and it is what catches a constraint whose declared floor the code no longer builds against. Run it in a job that does not commit its result.
Platform is not a dimension here. Dart documents the lockfile as the concrete versions of every immediate and transitive dependency, with no per-platform entry, so a lock written on macOS installs on a Linux runner unchanged.
Verified 2026-07-31 against the pubspec file, dart pub downgrade, and the pub glossary.
Static analysis (R-SEC-09)
Section titled “Static analysis (R-SEC-09)”No static analyzer documented for Dart detects vulnerability classes.
CodeQL’s supported languages list does not mention Dart. GitLab’s SAST supported languages table does not list Dart under any analyzer. Dart’s own analyzer is documented in terms of errors, warnings, and info-level diagnostics from the language specification plus a set of linter rules, with no security or taint analysis described anywhere in those pages.
Source: CodeQL supported languages and frameworks, GitLab SAST, and Customizing static analysis.
The strongest documented fallback is dart analyze --fatal-infos as a required check on pull requests, which catches correctness and style classes and no vulnerability class, plus a scanner over pubspec.lock for the dependency half described below. Say plainly which of the two a green check represents.
That fallback sits below what R-SEC-09 asks for, and the rule’s own clause covers it: a repository holding no source in a language a static analyzer supports falls outside the rule rather than failing it. Report Dart source as outside the rule on that clause and record this reading as the reason, rather than reporting a violation nobody can fix. A Dart entry appearing in either CodeQL’s or GitLab’s supported languages table retires the gap.
Vulnerability watch (R-SEC-11)
Section titled “Vulnerability watch (R-SEC-11)”The dependency graph parses pubspec.lock as its recommended file, with pubspec.yaml as an additional file. The pub row marks static transitive dependencies, Dependabot graph jobs, and automatic dependency submission all unsupported, so the coverage is what the committed lockfile lists.
Advisories come from the GitHub Advisory Database, which names this ecosystem Pub against the pub.dev registry.
osv-scanner reads pubspec.lock, which is also what covers a library package that follows the convention above and commits no lockfile: nothing watches such a repository’s own resolution, because it has none to watch, and the consumer’s application lockfile is where that resolution becomes real.
Verified 2026-07-31 against Dependabot supported ecosystems and repositories, Dependency graph supported package ecosystems, GitHub Advisory Database, dart pub get, Package layout conventions, dart analyze, Customizing static analysis, CodeQL supported languages and frameworks, GitLab SAST, Renovate managers, and osv-scanner supported languages and lockfiles.
Concrete flow for the decisions SKILL.md makes, for a Dart or Flutter package published to pub.dev. pub.dev documents one OIDC provider for automated publishing: GitHub Actions. The publisher is configured on the package’s own Admin tab and is bound to a git tag pattern rather than to a workflow filename, which is the field that differs most from every other registry in this directory.
The Dart team maintains a reusable workflow that does the whole publish, and it takes an environment input, so the approval gate in Step 4 works without hand-writing the job. That is worth knowing before writing anything, because a job that calls a reusable workflow cannot set environment: itself.
Source: Dart, Automated publishing of packages to pub.dev.
Contents
Section titled “Contents”- Gather facts (Step 1)
- Configure trusted publishing (Step 2)
- Write the hardened release workflow (Step 3)
- Gate on manual approval (Step 4)
- Verify provenance (Step 5): a gap, not a check
- Describe and sign what the release attaches (Step 6)
- Not yet published packages
Gather facts (Step 1)
Section titled “Gather facts (Step 1)”Read pubspec.yaml for name and version, and for repository or homepage, which is where the forge URL usually lives; fall back to git remote get-url origin. Note whether the package belongs to a verified publisher, because that decides who can configure automated publishing and what pub.dev displays about the package.
Check whether the package is already published, and read its published versions, from pub.dev’s own package endpoint:
curl -s https://pub.dev/api/packages/<name>The response lists every version with its version, archive_url, archive_sha256, and retracted flag. A 404 means the name is free. This endpoint comes from the Hosted Pub Repository Specification V2 rather than from pub.dev’s own supported-API list, which is worth knowing before a checker depends on it.
Note whether pubspec.lock is committed. Dart writes it by default and documents that an application package should commit it while a library should not, so its absence in a library is correct rather than a gap; R-SEC-08 and oss-harden own that.
Configure trusted publishing (Step 2)
Section titled “Configure trusted publishing (Step 2)”Only one of the two sections below is a flow. pub.dev documents GitHub Actions and says nothing about GitLab, so a GitLab project takes the fallback rather than a shorter version of the same setup.
GitHub Actions
Section titled “GitHub Actions”On pub.dev, open the package’s Admin tab and find the Automated publishing section. Enter:
- Repository: the owner and repository as
<owner>/<repo> - Tag pattern: a pattern containing
{{version}}, for examplev{{version}}
The tag pattern is the whole of the binding. A tag matching v{{version}} allows a GitHub Actions run triggered by git tag v1.2.3 to publish version 1.2.3, and only if pubspec.yaml declares that same version. There is no workflow filename field here, so unlike npm, PyPI, RubyGems, crates.io, and NuGet, the identity pub.dev checks does not name which workflow ran.
That makes the optional field the important one. Enable “Require GitHub Actions environment” and set the environment name to release. Without it, any workflow in the repository that can produce a matching tag and request an OIDC token can publish. With it, only a run in that environment can, which is what makes the approval gate in Step 4 load-bearing rather than decorative.
pub.dev requires no particular name here and its own examples use pub.dev. This skill writes release for every ecosystem, so the same word means the same thing in every repository it touches. What matters is that this field and the reusable workflow’s environment input are the same string; where a repository already has a working entry naming something else, keep it rather than renaming both to match this file.
In the workflow, the publish job needs permissions: id-token: write, which is what lets it request the OIDC token pub.dev validates.
GitLab CI/CD: no documented flow
Section titled “GitLab CI/CD: no documented flow”Dart’s automated publishing page documents GitHub Actions in detail, adds Google Cloud Build, and mentions GitLab CI/CD nowhere. Read that page before reporting on it: the absence is silence rather than a documented refusal, and it does not mean a flow exists under another name.
Source: Dart, Automated publishing of packages to pub.dev.
Where the page names no GitLab provider, this is the strongest documented alternative. Dart’s own answer for a CI system with no supported identity is a Google Cloud service account: create one, grant it publishing rights on pub.dev, and exchange its exported key for a pub.dev token.
gcloud auth activate-service-account --key-file=key-file.jsongcloud auth print-identity-token --audiences=https://pub.dev | dart pub token add https://pub.devdart pub publish --forceThe exported key is a long-lived credential and Dart’s documentation says to treat it like a password, because anyone who reads it can publish the package. Store it as a GitLab CI/CD variable that is masked, protected, and scoped to a protected environment with approval rules, so the key is unreadable outside an approved release job.
This is well below the bar R-PUB-02 sets: an exported service account key is a stored credential in exactly the sense the rule exists to eliminate, and it is account-shaped rather than package-shaped. It is also worse than the scoped-token fallbacks elsewhere in this directory, so say so plainly. Take it only while the page names no GitLab provider, and re-read that page before each release process is written.
Write the hardened release workflow (Step 3)
Section titled “Write the hardened release workflow (Step 3)”The Dart team publishes a reusable workflow that does the checkout, SDK setup, dependency resolution, dry run, and publish. Use it rather than hand-rolling the same steps, because it is first-party to the SDK and the OIDC token it requests is provisioned by dart-lang/setup-dart inside it:
name: Releaseon: push: tags: - 'v*'jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: dart-lang/setup-dart@v1 - run: dart pub get --enforce-lockfile - run: dart analyze --fatal-infos - run: dart test # oss-ci decides the actual command from CONTRIBUTING.md (R-CI-02)
publish: needs: [test] permissions: id-token: write uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 with: environment: releasedart pub get --enforce-lockfile in the test job is R-SEC-08’s frozen mode, so a workflow this skill writes with a bare dart pub get fails a rule the kit owns. It needs a committed pubspec.lock. Dart’s own guidance tells a library package not to commit one, and R-SEC-08’s second clause places that case outside the rule rather than failing it, so drop the flag for a library and keep it for an application. Do not add the flag and leave the lockfile uncommitted: the job then fails on every run with nothing to fix.
The trigger is v* rather than an exact three-part pattern, which is what every other reference in this directory uses and what pub.dev’s v{{version}} already covers. A narrower workflow trigger adds no safety here, because pub.dev checks the tag against its own pattern and refuses anything that does not match; what it does instead is silently skip a prerelease tag such as v1.2.3-beta, which is a valid pub version. The tag trigger still has to be no narrower than the pattern configured in Step 2, since the pattern is what pub.dev checks. There is no separate version comparison step here: dart pub publish refuses to publish a version that does not match pubspec.yaml, and the tag pattern is what ties the tag to that version, so the comparison happens on the registry’s side rather than in a run: step.
Two things about that reusable workflow are worth telling the maintainer rather than leaving them to discover. It runs dart pub get and dart pub publish in the same job that holds the publish identity, so build and publish are not separated the way this skill separates them elsewhere; the bound on that is that Dart’s package resolution runs no package-supplied code, so resolving the tree does not execute a dependency. And it installs a Flutter SDK as well as a Dart one, so that a Flutter package and a pure Dart package go through the same path; a pure Dart project is therefore pulling in a toolchain it does not use.
Pin the reusable workflow the way oss-harden pins an action, to a commit SHA rather than to @v1, and pin the actions in the test job the same way. A job that calls a reusable workflow accepts a narrow set of keys, so environment: is passed through the workflow’s own environment input rather than set on the job, which is exactly what the with: block above does.
If an existing workflow reads a pub.dev token from repository secrets, remove it from the YAML now and tell the user to revoke it once the new flow is verified.
Gate on manual approval (Step 4)
Section titled “Gate on manual approval (Step 4)”Two settings have to agree, and both are needed.
On the forge, create the environment at https://github.com/<owner>/<repo>/settings/environments/new, named release to match Step 2, with required reviewers naming at least one person other than an automation account. Required reviewers work for public repositories on current GitHub plans; private or internal repositories need GitHub Enterprise Cloud. Pass the name through the reusable workflow’s environment input, as above.
Create it with the API rather than the form. Reviewers and the tag policy are both settable, so nothing here needs a browser.
ENV=pub.devGHUID=$(gh api user --jq .id)gh api -X PUT "repos/{owner}/{repo}/environments/$ENV" \ -F wait_timer=0 \ -F prevent_self_review=false \ -f 'reviewers[][type]=User' -F "reviewers[][id]=$GHUID" \ -F 'deployment_branch_policy[protected_branches]=false' \ -F 'deployment_branch_policy[custom_branch_policies]=true'gh api -X POST "repos/{owner}/{repo}/environments/$ENV/deployment-branch-policies" \ -f 'name=v*' -f type=tagThree details decide whether that runs. gh api substitutes {owner} and {repo} from the checkout it runs in. Use -F for the booleans and the reviewer id, because -f sends every value as a string and the endpoint rejects a quoted boolean. Do not name the shell variable UID: zsh marks it read only, so the assignment fails before gh runs.
reviewers[][id] takes a numeric user or team id rather than a login. A team needs type=Team and that team’s id.
On pub.dev, enable “Require GitHub Actions environment” with the same name. Without that half, the environment gates the run but pub.dev will still mint a token for a run that skipped it, so the gate is a forge convention rather than part of the registry’s identity check.
pub.dev has no registry-side approval gate of its own. Report R-PUB-04 as unmet when the forge plan provides no native gate, rather than substituting an unverified approval action.
Verify provenance (Step 5): a gap, not a check
Section titled “Verify provenance (Step 5): a gap, not a check”pub.dev serves no build provenance. There is no attestation object, no signature on a published archive, and nothing comparable to npm’s npm audit signatures or PyPI’s Integrity API. Its publishing documentation covers who may upload and what the package page displays about the publisher, and says nothing about verifying what was uploaded. Read both pages before reporting, and do not read the verified publisher badge as provenance: it says a domain was verified for the account, not that a particular workflow produced a particular archive.
Source: Dart, Publishing packages and Dart, Automated publishing of packages to pub.dev.
What pub.dev does serve is a digest. Every version in the package endpoint from Step 1 carries archive_sha256, the hex-encoded SHA-256 of the archive at archive_url, which the repository specification describes as the field that lets a client verify the integrity of what it downloaded:
curl -s https://pub.dev/api/packages/<name> \ | jq -r --arg v '<version>' '.versions[] | select(.version == $v) | .archive_sha256'That is integrity, not provenance. It confirms the archive has not changed since publication and says nothing about which commit or workflow produced it. Use it as the release check after the first tag-triggered publish, and report it as what it is.
The strongest substitute for provenance is a forge attestation over the archive the publish produced. dart pub publish --dry-run lists what would go into the archive but the reusable workflow does not hand a file to a later job, so attesting the published archive means downloading it from archive_url in a job after the publish and attesting those bytes. That is weaker than attesting what the build produced, because it attests what the registry served back; say so rather than presenting it as equivalent.
This sits below R-PUB-03, which asks for provenance tied to the exact published artifact and the workflow that built it. Report it as unmet with the registry limitation named. It retires the day pub.dev serves an attestation for a published version, which is the natural next step for a registry that already validates an OIDC identity at upload.
Describe and sign what the release attaches (Step 6)
Section titled “Describe and sign what the release attaches (Step 6)”Only for a release that attaches a built asset to the forge release. Publishing to pub.dev attaches nothing to the forge, and the source archives GitHub generates for a tag are not built assets, so a package that only publishes goes to Step 7 instead. What this section covers is a project that also attaches compiled Flutter or Dart binaries.
This reference names no SBOM generator for Dart. The ones in common use are third-party tools rather than part of the SDK, and a tool that reads the dependency tree inside the release workflow is one the maintainer vets before it goes there. pubspec.lock is the closest thing the project already has, and it is neither SPDX nor CycloneDX, so publishing it does not satisfy R-PUB-05’s format requirement; say that rather than presenting the lockfile as a bill of materials. A library that does not commit pubspec.lock, which is what Dart’s own guidance tells it to do, does not even have that.
Two rules apply here and they ask for different things. R-PUB-05 wants that inventory. R-PUB-06 wants the assets signed, or listed by hash in a signed manifest. A manifest of hashes answers the second and nothing about the first, so do not report R-PUB-05 as met by publishing one.
What answers R-PUB-05 without a generator, on GitHub, is the forge’s own export of the repository’s dependency graph, which is already SPDX and needs nothing installed. The gh api step below writes it into dist/, so it ships as a release asset for R-PUB-05 and is listed in SHA256SUMS and attested alongside the binaries for R-PUB-06, in a job separate from the one that built them:
github-release: runs-on: ubuntu-latest needs: [publish] permissions: contents: write id-token: write attestations: write artifact-metadata: write steps: - uses: actions/download-artifact@v8 with: name: binaries path: dist/ - run: gh api repos/${{ github.repository }}/dependency-graph/sbom --jq .sbom > dist/sbom.spdx.json env: GH_TOKEN: ${{ github.token }} - run: (cd dist && sha256sum *) > SHA256SUMS - uses: actions/attest@v4 with: subject-checksums: SHA256SUMS - run: gh release upload "$GITHUB_REF_NAME" dist/* SHA256SUMS env: GH_TOKEN: ${{ github.token }}State both of the export’s limits to the maintainer rather than leaving them to be discovered. It is GitHub only, so a GitLab project keeps this gap and R-PUB-05 stays unmet there with the reason named. And it covers the repository’s declared dependency graph rather than what is inside the asset, which is exact for the published package, because it declares its dependencies rather than bundling them, and an approximation for a compiled binary attached beside it. The graph resolves past the direct dependencies only where pubspec.lock is committed, which a library does not do, so a library’s export lists its direct dependencies alone and the report should say so. Read the output back once with gh api repos/<owner>/<repo>/dependency-graph/sbom --jq '.sbom.packages | length' before reporting the rule met: a graph the forge does not parse for this ecosystem returns a near-empty package list rather than an error.
sha256sum runs from inside dist/ so the names it writes are the names the assets carry on the release. subject-checksums makes every file in the manifest a subject of the attestation in its own right, by name and digest; attesting SHA256SUMS itself with subject-path would leave a consumer able to verify the manifest and nothing about the assets it lists.
A consumer verifies an asset, then checks the rest of the download against the manifest:
gh attestation verify <asset> --repo <owner>/<repo>sha256sum -c SHA256SUMSRun the first command against each asset downloaded, never against SHA256SUMS, which is a subject of nothing. --signer-workflow <owner>/<repo>/.github/workflows/release.yml pins which workflow the attestation must have come from. On GitLab CI/CD the forge attestation is unavailable, so a GitLab release can carry the same SHA256SUMS with nothing signing it; say that rather than presenting the file as provenance.
The four grants above are copied exactly, on this job only, and the workflow’s top-level block stays contents: read. Narrowing anything else, pinning each uses: to a commit SHA, and auditing the result are oss-harden’s.
Not yet published packages
Section titled “Not yet published packages”Automated publishing is configured on the package’s Admin tab, which does not exist until the package is published once. Have the maintainer publish the first version from their own machine with dart pub publish, after dart pub publish --dry-run reports clean, then configure Step 2 immediately. That first release has no OIDC identity behind it and every release after it does.
Step 7 is in SKILL.md: read each R-PUB rule’s Check: line against what this file produced, and fix what fails before reporting done.
Verified 2026-07-31 against Dart, Automated publishing of packages to pub.dev, Dart, Publishing packages, the reusable workflow at dart-lang/setup-dart, .github/workflows/publish.yml, and the archive_sha256 field in the Hosted Pub Repository Specification V2.
Version sources (R-CHG-03)
Section titled “Version sources (R-CHG-03)”pubspec.yaml’s version field is the source, and it is the only file in a published package that states one.
pubspec.lock is not a second source. Dart documents that an application package should check the lockfile into source control and a regular package, which is Dart’s word for a library, should not, because a library is expected to work with a range of dependency versions. A published library therefore has the pubspec, the tag, and the changelog to compare, and an application repository that also publishes has the lockfile as one more file to keep current.
A monorepo with several packages has one pubspec.yaml each, and each is its own release unit.
Version syntax (R-CHG-02)
Section titled “Version syntax (R-CHG-02)”Pub requires semantic versioning: “Pub requires versions to be formatted that way, and to play well with the pub community, your package should follow the semantics it specifies.” Two Dart conventions sit on top of that.
Before 1.0.0 the Dart community shifts the interpretation down one slot, so 0.1.2 to 0.2.0 is a breaking change, 0.1.2 to 0.1.3 is a new feature, and 0.1.2 to 0.1.2+1 is a change that does not affect the public API. The first of those agrees with what R-CHG-02 asks for during initial development, so a Dart package following its own community convention lands on the same bump.
The + suffix is the deviation to watch. It is SemVer build metadata, and Dart uses it as a real release channel before 1.0.0 while noting that suffixes such as -0 or -beta do not affect dependency resolution, and recommends avoiding + once the package reaches 1.0.0. A +1 release still needs a changelog entry and a tag, so it is a release for R-CHG-03 whatever the resolver does with it.
Major version in package identity (R-CHG-07)
Section titled “Major version in package identity (R-CHG-07)”pub.dev does not encode the major version in package identity. A package name is claimed once and holds every version, so R-CHG-07 does not reach this ecosystem.
Withdrawing a release (R-CHG-01)
Section titled “Withdrawing a release (R-CHG-01)”Retraction is the mechanism and it is time-boxed at both ends. A published version can be retracted within seven days of publication, and a retracted version can be restored within seven days of the retraction. Both windows are short enough that a maintainer who finds the problem in week two has no retraction to make.
Retraction does not remove anything: “Retraction isn’t deletion.” A retracted version stays visible on pub.dev under a Retracted versions section with a RETRACTED badge, a project whose pubspec.lock already names the version keeps resolving it, and a developer who wants to depend on a retracted version has to pin it in dependency_overrides in pubspec.yaml. pub.dev documents no deletion at all.
Past the seven-day window the response is the same as on any registry with no withdrawal: publish a fixed version and say so. [YANKED] on the changelog heading maps to a retraction, and after the window it is the only record of the withdrawal that exists, which is what makes leaving the entry in place matter.
Verified 2026-07-31 against Publishing packages, Package versioning, and Pub glossary.