Skip to content

Publish from a tag instead of from a laptop - #8

Open
sovanken wants to merge 3 commits into
mainfrom
chore/release-pipeline
Open

Publish from a tag instead of from a laptop#8
sovanken wants to merge 3 commits into
mainfrom
chore/release-pipeline

Conversation

@sovanken

@sovanken sovanken commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Releasing today means running flutter pub publish --force on the maintainer's machine against a long-lived refresh token in %APPDATA%/dart/pub-credentials.json. That token is the only thing between a laptop and the package's users, the release isn't reproducible, and there's no record of what was verified before it shipped.

This makes the tag the trigger.

The flow

git tag -a v1.3.0 -m "..." && git push origin v1.3.0
                    |
            [release.yml]
   1. tag == pubspec.yaml version?          -> else fail
   2. CHANGELOG has a [1.3.0] section?      -> else fail
   3. format / analyze / test / dry-run / pana
   4. publish via OIDC (no stored credential)
   5. GitHub Release from the changelog section

Step 1 exists because a mismatched tag would publish the wrong version under the right name, which pub.dev cannot undo. Step 2 exists because v1.1.0 sat tagged-but-unreleased for twelve days when that step was manual.

ci.yml also gains a Package & pub.dev score job, so packaging and score problems surface on the PR instead of at tag time when the version is already public.

Why assert_pana_score.py rather than --exit-code-threshold 0

I tried to validate the gate locally first. pana aborted — its sandbox runner rejects Windows drive-letter paths, and webpinfo was missing — and still exited 0. A gate that passes when the analysis crashed is worse than no gate, and would have looked identical to a working one.

So the score is asserted from pana's JSON instead: a missing score is a failure, and the section that lost points is named in the log. Both workflows install the webp tools first, since pana needs them to process the three screenshots pubspec.yaml declares — without them it logs SEVERE and can lose points pub.dev itself would grant.

The gate is set at full marks because the package currently scores 160/160.

Verified

Check Result
Tag guard v1.2.0 accepted, v9.9.9 rejected
Changelog extractor returns the 1.2.0 section; exits non-zero for a missing version
Score asserter passes at 160/160; fails at 150/160 naming the section; fails on a crashed run reporting no score
pub publish --dry-run passes
pana itself verified by this PR's CI — Windows cannot run it

That last row is the point of opening this as a PR: the new package job runs pana in the environment that actually matters, so if 160/160 doesn't hold on Ubuntu, this PR goes red before it merges rather than breaking your next release.

One-time setup needed before the next release

pub.dev → Admin → Automated publishing → repository sovanken/solar_iconkit, tag pattern v{{version}}. Until that's set the publish step will fail; every verification step before it works regardless. If you also enable "Require GitHub Actions environment" there, add environment: pub.dev to the job to match.

Type of change

  • Chore (tooling, CI)

No packaged code changes.

🤖 Generated with Claude Code

sovanken and others added 3 commits September 8, 2026 09:23
Releasing currently means running `flutter pub publish --force` on the
maintainer's machine against a long-lived refresh token in
%APPDATA%/dart/pub-credentials.json. That token is the only thing standing
between a laptop and the package's users, the release is not reproducible,
and there is no record of what was verified before it went out. It also
means a release can only happen where that credential lives.

.github/workflows/release.yml replaces it. Pushing a vX.Y.Z tag:

1. verifies the tag matches pubspec.yaml — a mismatch would publish the
   wrong version under the right name, which pub.dev cannot undo;
2. verifies CHANGELOG.md has a section for it, so release notes are the
   changelog rather than a second copy that drifts (v1.1.0 previously sat
   tagged-but-unreleased for twelve days because that step was manual);
3. runs format, analyze, tests, `pub publish --dry-run` and pana;
4. publishes with a short-lived OIDC token (id-token: write), so no
   credential exists outside the run;
5. creates the GitHub Release from the changelog section.

ci.yml gains a "Package & pub.dev score" job so packaging and score
problems surface on the PR rather than at tag time, when the version is
already public.

tool/assert_pana_score.py exists because `pana --exit-code-threshold 0` is
not trustworthy on its own: running it locally, pana aborted with a Windows
sandbox path error and still exited 0. The script reads pana's JSON, treats
a missing score as failure, and names the section that lost points. Both
workflows also install the webp tools first — pana needs them to process
the three screenshots pubspec declares, and without them it logs SEVERE and
can lose points pub.dev would grant.

The gate is set at full marks because the package currently scores 160/160.

Verified: tag guard accepts v1.2.0 and rejects v9.9.9; the changelog
extractor returns the 1.2.0 section and exits non-zero for a missing
version; the score asserter passes at 160/160, fails at 150/160 naming the
section, and fails on a crashed run that reported no score. The pana run
itself is verified by this PR's own CI, since Windows cannot run it.

Requires one pub.dev setting before the next release: Admin -> Automated
publishing -> repository sovanken/solar_iconkit, tag pattern v{{version}}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Not for merge — identifies why a fresh Linux checkout reports
analysis_options.yaml as modified when the local tree is clean.
The new packaging gate failed on its first run, and not on the score:
`pub publish --dry-run` exits 65 because a fresh Linux checkout reports
analysis_options.yaml and example/analysis_options.yaml as modified.

Cause: Flutter rewrites both files during `pub get`. 3.47.2 (current
stable, used by the unpinned job) adds `- build/**` to the root analyzer
exclude and prepends a whole `analyzer: exclude: [build/**, android/**]`
block to the example's. Flutter 3.41.4 locally does not, which is why the
tree looked clean on my machine and why this never showed up before.

Fixed by committing exactly what Flutter writes, byte-for-byte, so it has
nothing left to add and the tree stays clean on any version. Excluding
build/ and android/ from analysis is correct regardless.

Worth noting this would have broken the next release rather than just this
PR: release.yml runs the same --dry-run step. It went unnoticed until now
only because publishing was done by hand with --force, which skips exactly
this validation.

Also removes the temporary diagnostic step that found it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant