release

GitHub

Flutter Gemma插件发布技能,执行版本升级、原生预编译包重发布及SHA256校验和更新。强制要求代码与文档合并提交,通过自动化钩子确保README和网站文档同步更新,并严格遵循完成定义清单以保障发布质量。

.claude/skills/release/SKILL.md DenisovAV/flutter_gemma

Trigger Scenarios

需要发布新版本时 触发 /release 命令

Install

npx skills add DenisovAV/flutter_gemma --skill release -g -y
More Options

Non-standard path

npx skills add https://github.com/DenisovAV/flutter_gemma/tree/main/.claude/skills/release -g -y

Use without installing

npx skills use DenisovAV/flutter_gemma@release

指定 Agent (Claude Code)

npx skills add DenisovAV/flutter_gemma --skill release -a claude-code -g -y

安装 repo 全部 skill

npx skills add DenisovAV/flutter_gemma --all -g -y

预览 repo 内 skill

npx skills add DenisovAV/flutter_gemma --list

SKILL.md

Frontmatter
{
    "name": "release",
    "description": "Release flutter_gemma — bump versions, optionally re-publish native prebuilts (iOS\/macOS\/Linux\/Windows\/Android dylibs) to GitHub Release, update SHA256 checksums in hook\/build.dart, publish to pub.dev",
    "user_invocable": true
}

Flutter Gemma Release

Run as /release <plugin-version> (e.g. /release 0.14.1).

⛔ HARD RULES — these are not advice, and not overridable by your judgment

A checklist advises; these refuse. They exist because the steps below were read and still skipped — twice in one session (agent 0.2.3 shipped without its README → had to publish 0.2.4; the 1.5.9 docs were split into a separate website PR). When a rule here conflicts with what seems "more sensible right now", the rule wins. If following one is genuinely impossible, SAY SO out loud and stop — do not silently do the other thing.

  1. ONE PR: code + website/ docs + version bumps ship together. The Step 12 doc changes (version pins + new/changed-API docs) go on the SAME release branch/PR as the code, merged in one go. A separate website-only PR is FORBIDDEN except (a) the code PR has ALREADY merged before you got to docs (a planning failure — do NOT let this become the pattern), or (b) a post-merge deploy-failure hotfix, which 12c requires be a new PR since main is protected. Case (a) is a failure to note in the PR, not the pattern to copy. Author the docs BEFORE the release PR merges.
  2. Docs are part of Done, not a follow-up. "I'll do the docs later / in a follow-up" is the exact failure this skill exists to stop. README.md (pub.dev-facing) and website/content/docs/** are release deliverables. A guard-release-docs.py hook BLOCKS dart pub publish when a package's lib/ changed but neither its README nor website docs did — if it fires, you skipped Step 12, not "hit a false positive".
  3. Reproduce this Definition-of-Done in your reply and mark every item (done / N/A + reason) before you publish. Do not publish off memory of the skill — walk it as a literal checklist against the actual repo state.

Definition of Done (paste it; check 1a–12b before Step 10 publish; 12c is verified after merge)

[ ] Pre-flight: git clean · analyze 0 err · flutter test green · build web + one native target
[ ] 1a  every package whose lib/ changed is in the publish list (grep, don't guess)
[ ] 1b/c native: dylibs/build-scripts changed? → rebuild + SHA256 + native release, else N/A
[ ] 5b  manifest gate RUN and printed "N platform(s) compared" — N == number of tarballs
[ ] 1e  core public API changed? → upgrade-genkit (realign + version), else N/A
[ ] 1f  shared code duplicated across satellites patched everywhere (grep the pattern)
[ ] 1g  each changed satellite's flutter_gemma: floor >= the core version it now needs
[ ] 2   versions bumped: pubspec + podspec (if any) + CLAUDE.md Current-Version line
[ ] 7   CHANGELOG: one short line per package, every published package
[ ] 8   dart pub publish --dry-run → 0 warnings, every package
[ ] 12a website + README version pins bumped to the just-published versions
[ ] 12b new/changed public API + behavior documented (README + website)  ← SAME PR
[ ] 12c after merge: firebase-hosting-merge run == success (not just triggered)

Architecture context (read this first)

flutter_gemma 0.14.0+ has no Kotlin/JVM/gRPC server. Native libs come from one of two sources, decided per-platform by hook/build.dart (Native Assets):

  1. Local prebuilts at packages/flutter_gemma_litertlm/native/litert_lm/prebuilt/<os>_<arch>/ — populated locally by packages/flutter_gemma_litertlm/native/litert_lm/build_*.sh scripts. NOT tracked in git (gitignored since 0.14.3 — keeps clones lean) and excluded from the pub package via .pubignore. Maintainers regenerate them on demand and upload to a GitHub Release.
  2. GitHub Release native-v<NATIVE_VERSION> archives (e.g. native-v0.10.2-a) — the canonical source for both end users and CI. URL pattern: litertlm-<os>_<arch>.tar.gz flat archive of the matching prebuilt/ folder. End users fetch from there at pub get time via hook/build.dart. Maintainers re-fetch from there too if their local prebuilt/ is missing (gh release download native-v<X> then extract — see Step 5).

Whether to bump native-v<NATIVE_VERSION> or re-publish the existing tag is the central decision of every release.

Pre-flight

git status                  # all desired changes staged or already committed
git log --oneline -5
flutter analyze             # 0 errors
flutter test                # all pass

# Cross-platform compile sanity — analyze/test run on host VM and skip
# conditional imports (e.g. `lib/core/ffi/*_stub.dart`). The only thing
# that catches stub/client signature drift is `flutter build <target>`.
# Skipping this is how `enableSpeculativeDecoding` web breakage shipped
# in 0.15.0 — analyze was green, tests passed, web build threw
# `No named parameter ...` at dart2js time.
cd packages/flutter_gemma/example
flutter build web --no-tree-shake-icons
# Android MUST be built --release, not --debug: a release build runs R8
# (shrink/minify/obfuscate) and the full native-asset packaging path, which
# debug skips. Bugs that only surface under R8 (stripped classes, missing
# keep rules, native lib packaging) are invisible to `--debug`.
flutter build apk --release
flutter build macos --debug
flutter build ios --no-codesign --debug
cd ../../..   # back to repo root

Step 1: Determine release scope

Three independent dimensions — answer each:

1a. Plugin code changed?

# Pathspecs are repo-root relative. The pre-monorepo form (lib/ hook/ ios/ …)
# matches NOTHING here and `git diff` exits 0 with empty output, so the whole
# scope decision silently answers "nothing changed".
git diff <last-tag> --stat -- 'packages/*/lib' 'packages/*/hook' \
  'packages/*/pubspec.yaml' 'packages/*/android' 'packages/*/ios' \
  'packages/*/macos' 'packages/*/darwin' 'packages/*/web'

If yes → bump pub plugin version, publish to pub.dev. Always true for a release. Then run 1f for every satellite whose copy of the touched code is stale — a fix is not "done" until every duplicate across all 6 packages is patched or shown N/A.

1b. Native dylibs changed (any platform)?

git diff <last-tag> -- packages/flutter_gemma_litertlm/native/litert_lm/prebuilt/

If any dylib changed → must re-publish GitHub Release archives and update SHA256 checksums in hook/build.dart, otherwise end users will keep getting the stale dylibs.

1c. patch_c_api.sh / build_*.sh / WORKSPACE patch changed?

This implies (1b) — verify dylibs were actually rebuilt against the new patches. If not, rebuild before continuing (see "Rebuild native dylibs" below).

1d. Website (website/, fluttergemma.dev) — ALWAYS in scope

Every release touches the site. At minimum the package versions hardcoded in its docs must be bumped to the just-published versions (Step 12a) — this is required even for a version-only release. On top of that, any new/changed public API, breaking change, or common pitfall must be documented (Step 12b). Don't defer to "later" — stale docs outlive the release.

1d-bis. README.md ships in the archive — a docs-only fix there needs its own version bump

Step 1a scopes "did the package change" to lib/ hook/ pubspec.yaml ios/ android/ web/, and 1d/12 scope "docs" to website/. packages/*/README.md is in neither, and it is the one doc that gets frozen into the published tarball and rendered as the pub.dev landing page. Fixing it on the branch changes nothing for users until a version is published.

What this rule prevents (core 1.5.8): 1.5.7 shipped with a README telling Windows users their discrete GPU crashes and to fall back to CPU/NPU — published the same day the release that fixed it was being prepared. The website was corrected, DESKTOP_SUPPORT.md was corrected, and both reach users immediately via GitHub. The README needed 1.5.8.

Two consequences:

  1. Grep the WHOLE repo for any claim you are correcting, not just website/. Same sentence, several homes, different delivery channels:
    grep -rniE "<the claim you are fixing>" --include='*.md' packages/ website/ | grep -v '/build/'
    
    A single stale statement in this repo lived in eight places: the litertlm README, the core README twice, DESKTOP_SUPPORT.md three times, and two site pages.
  2. Know which docs actually ship before deciding whether a bump is needed — the answer differs per package:
    cd packages/<pkg> && dart pub publish --dry-run 2>&1 | grep -E "^├── .*\.md"
    
    In flutter_gemma only README.md and CHANGELOG.md ship; DESKTOP_SUPPORT.md, MIGRATION.md and CONTRIBUTING.md are GitHub-only and need no publish.

1e. Did the core public API change? → realign the Genkit packages

git diff <last-tag> -- packages/flutter_gemma/lib/flutter_gemma_interface.dart packages/flutter_gemma/lib/core

If the public InferenceModel / InferenceChat / EmbeddingModel / Message / ModelResponse / enum surface changed, the Genkit integration packages (genkit_flutter_gemma, genkit_hybrid) likely no longer compile or are missing the new features. Run the upgrade-genkit skill before publishing — it realigns converters + the test fakes (which must match upstream signatures) and bumps those packages. They release in lockstep with the monorepo, so don't ship a core change that leaves them broken.

1f. Did a fix touch shared code duplicated across satellites? → propagate it to ALL packages

The monorepo split means the SAME logic is often copy-pasted into multiple packages' android/build.gradle, hook/build.dart, iOS podspecs, or FFI stubs. A core-only fix that leaves a copy stale ships a HALF-fix — and because each satellite publishes independently, the stale copy reaches users under its own version number.

Regression #360 (what this rule prevents): the AGP-9 Kotlin guard if (agpMajor < 9) was fixed in flutter_gemma/android/build.gradle, but the byte-identical guard in flutter_gemma_mediapipe/android/build.gradle was missed. mediapipe 1.0.3 shipped to pub.dev still broken, so every .task user on AGP 9 kept hitting the crash the core fix was supposed to close.

Before finalizing, grep the pattern you changed across ALL packages and confirm every copy is patched (or provably N/A):

grep -rn "<the exact pattern you changed>" packages/
# e.g. for the #360 guard:
grep -rn "agpMajor < 9" packages/*/android/build.gradle

Shared-code hotspots to sweep, per fix type:

  • Android Gradlepackages/*/android/build.gradle (only flutter_gemma + flutter_gemma_mediapipe have one): kotlin-android guard, compileSdk, minSdkVersion, kotlin_version, AGP classpath.
  • Native hookpackages/flutter_gemma_litertlm/hook/build.dart (the only hook that owns a bundle): the _litertlmBundle version: and checksums: fields, _cacheBaseDir() cache-busting, stage() Apple-only guard.
  • iOS podspecspackages/*/ios/*.podspec: s.version, min-iOS, dep pins, vtool minos on any bundled dylib.
  • FFI / web stubslib/**/*_stub.dart: conditional-import signatures that analyze/test can't catch (web stub drift).

Every affected satellite gets its own version bump + CHANGELOG entry + publish. This makes it a MULTI-package release — before touching any version, list every package you will publish (e.g. "publishing flutter_gemma 1.2.2 AND flutter_gemma_mediapipe 1.0.4"), and run the whole of Steps 2/8/9/10 for each one.

1g. Did a satellite start CALLING a newer core API than its flutter_gemma: floor allows? → bump the floor

Each satellite (agent / speech / litertlm / mediapipe / embeddings / rag) declares a flutter_gemma: ^X.Y.Z constraint. In the pub workspace the local core is always used, so flutter analyze / flutter test and dart pub publish --dry-run all pass with a too-low floor — everything builds green locally and dry-run only checks the constraint is satisfiable, never that the code needs a higher one. A pub.dev consumer who pins an older core alongside the new satellite then gets a resolve that fails to compile (calls a method/param that core version lacks). This is a manual step; nothing automated catches it.

Regression this prevents (agent 0.2.2): AgentLoop was rewritten to call generateChatResponseWithTools(onMaxToolTurns:)onMaxToolTurns landed in core 1.5.5 — but the satellite still declared flutter_gemma: ^1.2.0. A fresh install resolves core to latest (fine), but a consumer on flutter_gemma: 1.4.0 + flutter_gemma_agent: 0.2.2 resolves happily then throws No named parameter 'onMaxToolTurns' at build time.

For every satellite whose own Dart code changed, bump its flutter_gemma: floor to the core version that introduced the newest core symbol it now uses:

git diff <last-tag> -- packages/<satellite>/lib   # what core symbols did it start using?
grep -n 'flutter_gemma: \^' packages/<satellite>/pubspec.yaml

Step 2: Bump versions

Always:

File Field Note
pubspec.yaml version: the plugin version (e.g. 0.14.1)
podspecs — all four, they drift independently s.version match the owning package's version. packages/flutter_gemma/ios/flutter_gemma.podspec, packages/flutter_gemma/macos/flutter_gemma.podspec, packages/flutter_gemma_mediapipe/ios/flutter_gemma_mediapipe.podspec, packages/flutter_gemma_builtin_ai/darwin/flutter_gemma_builtin_ai.podspec. Verify with the loop below rather than by eye — core's iOS and macOS podspecs were four and five releases behind when this was last checked.
for ps in packages/*/{ios,macos,darwin}/*.podspec; do
  [ -f "$ps" ] || continue
  pkg=$(echo "$ps" | cut -d/ -f2)
  want=$(grep -m1 '^version:' "packages/$pkg/pubspec.yaml" | awk '{print $2}')
  got=$(grep -m1 "s.version" "$ps" | sed "s/.*'\(.*\)'.*/\1/")
  [ "$want" = "$got" ] && s=OK || s="DRIFT (pubspec $want)"
  printf '  %-64s %-8s %s\n' "$ps" "$got" "$s"
done

| CLAUDE.md | Current Version: line | match plugin version |

Only if (1b) bumps NATIVE_VERSION:

File Field
packages/flutter_gemma_litertlm/hook/build.dart the version: field of _litertlmBundle (a _NativeBundle) — bump (e.g. '0.16.0''0.16.1'). There is no _nativeVersion identifier; grep for version: inside const _litertlmBundle. The cache dir helper is _cacheBaseDir().

For App Store / breaking platform fixes prefer bumping NATIVE_VERSION rather than overwriting native-v0.10.2 assets — keeps consumers on 0.14.0 reproducible. Overwrite only for emergency hotfixes where downstream version pinning is acceptable.

Step 3: Rebuild native dylibs (if needed)

Per-platform rebuild scripts. bazelisk clean --expunge between rebuilds only if patch_c_api.sh / WORKSPACE patch changed (forces patch_cmds re-run on a fresh extraction). Otherwise incremental.

# Always pass the pinned tag SHA explicitly. Every script has a DEFAULT_REF and
# every DEFAULT_REF lags the release you are migrating to — build_ios.sh still
# defaulted to a v0.14.0-era commit during the v0.15.0 migration. Get the SHA
# with: gh api repos/google-ai-edge/LiteRT-LM/git/ref/tags/<tag> --jq '.object.sha'
SHA=<pinned tag SHA>
N=packages/flutter_gemma_litertlm/native/litert_lm

# macOS arm64
"$N/build_macos.sh" "$SHA"
# iOS device + simulator
"$N/build_ios.sh" "$SHA"
# Android arm64 (cross-compile from macOS) — needs NDK r29
"$N/build_android.sh" "$SHA"
# Android Qualcomm NPU dispatch (separate target, from the LiteRT repo)
"$N/build_qualcomm_dispatch.sh" "$SHA"
# Linux x86_64 — on a Linux VM (use GCloud per project_gcloud_vm_workflow memory)
# Windows x86_64 — on a Windows VM (same)

Verify each rebuilt dylib:

nm -gU prebuilt/<os>_<arch>/libLiteRtLm.dylib | grep litert_lm_engine_create  # must export
otool -D prebuilt/<os>_<arch>/libLiteRtLm.dylib                              # @rpath/libLiteRtLm.dylib

If patches changed — also verify patch markers are baked into the binary, e.g.:

strings prebuilt/ios_arm64/libLiteRtLm.dylib | grep '@executable_path'

Step 4: Pack tar.gz archives

Each archive is a flat tar of the matching prebuilt/ directory. Naming: litertlm-<os>_<arch>.tar.gz.

PREBUILT=packages/flutter_gemma_litertlm/native/litert_lm/prebuilt
DIST=$(mktemp -d)
# All SEVEN platforms the hook has checksums for. linux_arm64 was missing from
# this list for six native releases: the loop silently skipped it (`if -d`
# guards a directory that is there), the Step-5 glob then inherited the gap
# into checksums_litertlm.txt, and the result is a release that is internally
# consistent and still unbuildable on that platform.
PLATFORMS="macos_arm64 ios_arm64 ios_sim_arm64 android_arm64 linux_x86_64 linux_arm64 windows_x86_64"
for d in $PLATFORMS; do
  if [ -d "$PREBUILT/$d" ]; then
    (cd "$PREBUILT/$d" && tar -czf "$DIST/litertlm-$d.tar.gz" .)
    echo "  $d: $(ls -la "$DIST/litertlm-$d.tar.gz" | awk '{print $5}') bytes"
  else
    echo "  $d: NO prebuilt dir — will not be in this release"
  fi
done

Only archive platforms whose dylibs actually changed since the previous release. Untouched platforms keep their existing release assets — but if you are cutting a new native-v* tag, every platform must be present, because the hook verifies against its own checksums map regardless of which ones you touched. Assert it rather than eyeballing the loop's output:

# Count what you packed against what the hook demands.
want=$(grep -cE "'litertlm-[a-z0-9_]+\.tar\.gz'" packages/flutter_gemma_litertlm/hook/build.dart)
got=$(ls "$DIST"/litertlm-*.tar.gz 2>/dev/null | wc -l | tr -d ' ')
[ "$got" -eq "$want" ] || { echo "packed $got, hook expects $want"; exit 1; }

Step 5: Compute SHA256 + update hook/build.dart

for f in "$DIST"/litertlm-*.tar.gz; do
  printf "  '%s':\n      '%s',\n" "$(basename "$f")" "$(shasum -a 256 "$f" | awk '{print $1}')"
done

Paste each <filename>: <sha256> into the matching entry in hook/build.dart _checksums map. Update only the platforms whose dylibs you actually rebuilt — leave the others.

Also regenerate checksums_litertlm.txt for the GitHub Release page (single text file with sha256 filename lines):

(cd "$DIST" && shasum -a 256 litertlm-*.tar.gz > checksums_litertlm.txt)

Step 5b: ⛔ Run the manifest gate BEFORE uploading anything

The archives now exist and nothing is published yet — this is the only moment the gate can still save you. It diffs each new tarball's file list against the same archive in the previous tag and fails on any file that disappeared.

packages/flutter_gemma_litertlm/native/litert_lm/verify_tarball_manifest.sh \
  "$DIST" native-v<PREVIOUS>     # e.g. native-v0.14.0

Read the last line, not the exit code alone. A pass now states how many platforms it compared:

✅ MANIFEST CHECK PASSED — 7 platform(s) compared against native-v0.14.0, no unexplained file drops.

If that number is lower than the number of tarballs you packed, the gate did not examine the rest — treat it as a failure and find out why. exit 2 means it could not read the tag at all (bad auth, wrong tag name); it deliberately refuses to report a pass in that case, because a check that compared nothing is indistinguishable from a check that found nothing wrong.

This gate was written in 1.0.1 (ba614096) as the answer to native-v0.13.1 shipping without the Qualcomm/QNN and Intel OpenVino stacks — and then no release step ever called it, so it sat unused through native-v0.16.0. That is why it is a numbered step with a checklist line rather than a suggestion.

Step 6: Update GitHub Release assets

⛔ NEVER overwrite a tag referenced by a published plugin version

gh release upload --clobber on an existing native-v* / qdrant-edge-v* tag silently breaks every end user already on a plugin version whose hook/build.dart references that tag. The published SHA256 (in their pubspec.lock-pinned plugin code) no longer matches the bytes GitHub serves, the hook deletes the archive and returns null, the build succeeds with a missing CodeAsset, and the app crashes at runtime on first dlopen().

This is unrecoverable. tar -czf is not deterministic across runs (mtime, file ordering, gzip block boundaries differ), so even with every original dylib byte you cannot reproduce the original tar SHA256.

Always publish a new tag insteadnative-v0.10.3, not native-v0.10.2 reuploaded. The cost of a new tag is zero; the cost of breaking a shipped plugin version is real users with runtime crashes who cannot upgrade until the next release cycle.

See feedback_never_reupload_released_tarballs.md for the full incident write-up.

Always: new tag (native-v0.10.3)

Old native-v0.10.2 keeps working for old plugin versions. Need GitHub Release notes describing what changed.

RELEASE=native-v0.10.3
gh release create "$RELEASE" "$DIST"/litertlm-*.tar.gz "$DIST"/checksums_litertlm.txt \
  --title "Native dylibs $RELEASE" \
  --notes-file release-notes-native.md \
  --target main

Verify each URL returns HTTP 200 + sha256 matches:

for f in "$DIST"/litertlm-*.tar.gz; do
  name=$(basename "$f")
  url="https://github.com/DenisovAV/flutter_gemma/releases/download/$RELEASE/$name"
  curl -sI "$url" | head -1
  curl -sL "$url" | shasum -a 256 | awk '{print "  "$1"  '"$name"'"}'
done

⛔ Three-way checksum consistency — MANDATORY (regression: #316)

checksums_litertlm.txt is informational (the build hook does NOT read it — it verifies against the _checksums map baked into packages/flutter_gemma_litertlm/hook/build.dart). But a STALE txt is dangerous: in #316 a user (@remingtonc) hand-verified against the txt during a checksum-mismatch debug, the txt said e24804d9… while the actual asset was f809c5a2…, and it sent them down the wrong path. For every tag you touch, the same SHA must appear in all THREE places — the uploaded .tar.gz, checksums_litertlm.txt on the Release, and the hook's _checksums entry. Verify after upload:

HOOK=packages/flutter_gemma_litertlm/hook/build.dart
for f in "$DIST"/litertlm-*.tar.gz; do
  name=$(basename "$f")
  # 1. actual asset bytes served by GitHub
  asset=$(curl -sL "https://github.com/DenisovAV/flutter_gemma/releases/download/$RELEASE/$name" | shasum -a 256 | awk '{print $1}')
  # 2. what checksums_litertlm.txt on the Release claims
  txt=$(curl -sL "https://github.com/DenisovAV/flutter_gemma/releases/download/$RELEASE/checksums_litertlm.txt" | awk -v n="$name" '$2==n{print $1}')
  # 3. what the hook expects
  hook=$(grep -A1 "'$name'" "$HOOK" | grep -oE "[0-9a-f]{64}" | head -1)
  echo "$name:"
  echo "  asset=$asset"
  echo "  txt  =$txt   $([ "$asset" = "$txt" ] && echo OK || echo '❌ STALE TXT')"
  echo "  hook =$hook   $([ "$asset" = "$hook" ] && echo OK || echo '❌ HOOK MISMATCH — users will fail to build')"
done

All three must match for every platform you re-uploaded. If you re-uploaded a .tar.gz you MUST also re-upload a fresh checksums_litertlm.txt in the same gh release upload --clobber — never one without the other. (#316 is what a stale released tag looks like in the wild — see the ⛔ "NEVER overwrite a tag referenced by a published plugin version" rule above.)

Step 7: Update CHANGELOG.md

Add new section at top. Categories: App Store / packaging fixes, Features, Bug fixes, Breaking changes, Native runtime updates (if the bundle version: bumped). Reference issue / PR numbers (#245, #239).

Style: terse, one line per item, mirror 0.13.x pattern

The user has rejected verbose CHANGELOG entries multiple times. Write each bullet as one short sentence describing what was fixed and the issue/PR reference. Do NOT explain root cause, history, build details, or include workaround code blocks — that lives in commit messages and issue threads, not in CHANGELOG.

Bad (rejected):

- **Fix Apple companion dylib min iOS** (#245): `libGemmaModelConstraintProvider.dylib`
  was built upstream with `minos 26.2`, causing App Store Connect to reject any
  app whose `Info.plist` minimum iOS is below 26.2. Patched to `minos 14.0` post-
  download (other companion dylibs already on 14.0/16.0). Filed upstream;
  permanent fix needs Google rebuild.

Good (matches 0.13.x):

- **Fix App Store ITMS-90208 rejection on iOS** (#245): downgraded patched
  `libGemmaModelConstraintProvider.dylib` minos 26.2 → 14.0 to match other
  companion dylibs.

Rule of thumb: each entry ≤ 2 lines wrapped at 100 cols. If you need more to explain it, that's a sign it should be split into multiple entries or moved to a separate doc.

Step 8: Verify

flutter analyze
flutter test
# Cross-platform compile sanity (also in Pre-flight — rerun here after
# version bumps in case a setter/getter signature shifted):
(cd packages/flutter_gemma/example && flutter build web --no-tree-shake-icons)
(cd packages/flutter_gemma/example && flutter build apk --release)   # --release, not --debug: exercises R8 + native packaging
(cd packages/flutter_gemma/example && flutter build macos --debug)
(cd packages/flutter_gemma/example && flutter build ios --no-codesign --debug)
dart pub publish --dry-run     # 0 warnings (package size is informational — the
                               # FFI bindings + pigeon + example already push it
                               # to ~700 KB on 0.16.x; the old <=100 KB ceiling
                               # predates 0.14.0 and no longer applies)

NEVER publish without dry-run first. Publishing is IRREVERSIBLE.

Step 9: Commit + tag + push

git add <changed files>
git commit -m "0.14.1: <one-line summary>" \
           --author="Sasha Denisov <denisov.shureg@gmail.com>"
# No "Co-Authored-By: Claude" / no AI attribution

git tag v0.14.1
git push origin <branch> --tags

Step 10: pub.dev publish

⛔ ALWAYS publish from main, and main MUST be up to date — no exceptions

Publishing happens from the main branch only, never from a feature/release branch. After a PR merges, GitHub auto-deletes its head branch, so a dart pub publish still sitting on that branch is publishing code from a ref that no longer exists on origin — the guard-publish.py hook blocks this (refusing to publish source that is not on the remote … branch does not exist on origin). This is not a bug to work around; it is the guard doing its job.

The fix is always the same — get onto an up-to-date main first (Rule 2 says the user owns git, but switching to main for a release is an explicit, expected release action — do it, don't ask):

git switch main
git pull --ff-only origin main        # main MUST equal origin/main
# verify the release landed on main before publishing:
git rev-parse --short HEAD            # == origin/main
git show HEAD:packages/<name>/pubspec.yaml | grep '^version:'   # the version you're publishing
git status --short                    # clean tree

Only once HEAD == origin/main, the version on main is the one you intend to publish, and the tree is clean, proceed:

# Run from the package dir so the guard-release-docs hook can resolve which
# package is publishing (bare `dart pub publish` with the cwd already in the
# package works too — the hook reads the payload cwd — but the explicit `cd` is
# unambiguous):
cd packages/<name>
dart pub publish --dry-run    # verify once more (expect 0 warnings on a clean main)
dart pub publish --force      # only after user approval; --force is non-interactive

Step 11: Optional — GitHub plugin release

The .github/workflows/release.yml triggers on v*.*.* tag push and creates a GitHub Release with the example APK. Push the tag to fire it (already done in Step 9). Verify:

gh run list --workflow release.yml --limit 3
gh release view v0.14.1

Step 12: Reflect the release on the website (fluttergemma.dev)

MANDATORY ON EVERY RELEASE. The docs site lives in this repo at website/ (Jaspr static site → Firebase Hosting). Stale docs are a support-burden multiplier — every doc that still shows the old version or omits a new API generates issues.

12a. ALWAYS bump the package versions shown on the site (even for a pure version-only release)

The site hardcodes ^X.Y.Z in pubspec snippets across the docs — these MUST match the versions you just published, or new users copy-paste outdated deps. This is required every single release, regardless of whether code changed. Find every stale reference:

cd website
grep -rnE "flutter_gemma[a-z_]*: *\^?[0-9]+\.[0-9]+\.[0-9]+" content/

Update each ^X.Y.Z for the core packages (flutter_gemma, flutter_gemma_litertlm, flutter_gemma_mediapipe, flutter_gemma_embeddings, flutter_gemma_rag_qdrant, flutter_gemma_rag_sqlite) AND the Genkit integration packages (genkit_flutter_gemma, genkit_hybrid) to the just-published versions. Common spots: installation.md, getting-started.md, migration.md, packages.md, genkit.md. Cross-check against pub.dev so the site never lags the published packages.

12b. Update docs for any behavior/API change

  • New / changed public API → the topic doc that covers it (e.g. a new createSession param → getting-started.md; multimodal → multimodal.md; models → models.md).
  • Breaking changes / migrationsmigration.md.
  • A bug class users hittroubleshooting.md (e.g. the #318 maxTokens vs maxOutputTokens confusion belongs here).

12c. Deploy — it's automatic on merge to main

You do NOT run a manual deploy. .github/workflows/firebase-hosting-merge.yml auto-deploys to Firebase Hosting (aichat-c0c27, target fluttergemma, https://fluttergemma.dev → live channel) on every push to main that touches website/** or packages/flutter_gemma/example/**. So:

  1. PRE-MERGE (do this on the branch, before merging): build the Jaspr SSG locally to catch a build-time crash BEFORE it takes down the live deploy. The CI job runs the exact same jaspr build, so if it fails locally it will fail in CI — but locally you fix it in a branch instead of leaving main deployed from the old commit.
    cd website && jaspr build    # must end with "Completed building project"; NO "[ERROR]"
    
    The most common breakage is a code fence in a .md doc with a language the highlighter can't parse. syntax_highlight_lite (via jaspr_content) only ships a Dart grammar — a ```yaml/```xml/```kotlin/```bash fence throws Null check operator used on a null value in Highlighter and fails the whole SSG build → the site stays stuck on the previous version. Every existing doc uses only ```dart; for any other language use a plain fence (``` with no language tag). Grep before you merge:
    grep -rhoE '```[a-z]+' website/content/docs/*.md | sort | uniq -c   # expect: only ```dart
    
  2. Commit the website/ changes (same author rule, no AI attribution) on your release branch / PR.
  3. When the PR merges to main, the workflow builds the Jaspr SSG + the Flutter web example (/try) and deploys automatically.
  4. VERIFY THE MERGE DEPLOY ACTUALLY SUCCEEDED — do NOT assume merge == deployed. The run failing on the "Build Jaspr site (SSG)" step is silent: pub.dev shows the new package, but fluttergemma.dev still serves the OLD build. Check the run conclusion, not just that it triggered:
    gh run list --workflow firebase-hosting-merge.yml --limit 3   # newest must say "success", not "failure"
    # if failure: gh run view <id> --log-failed | grep -iE 'error|Null check|Highlighter'
    # then open https://fluttergemma.dev/docs/... and confirm the NEW change is live
    
    If the deploy failed, the fix is another PR (main is a protected branch — you CANNOT push a hotfix directly; branch + PR + merge, same as any change).

A manual ./deploy.sh exists in website/ for local one-off deploys (it does the same build + firebase deploy), but the merge workflow is the normal path — don't run it by hand unless the workflow is broken. The site is NOT on pub.dev; dart pub publish never touches it — only this workflow (or deploy.sh) does.

Common gotchas

  • Website SSG build fails silently on a non-Dart code fence — a ```yaml/```xml/```kotlin fence in any website/content/docs/*.md crashes the Jaspr highlighter (Dart-only grammar) → the merge deploy fails → fluttergemma.dev stays on the OLD build while pub.dev shows the new package. Always jaspr build the site locally on the branch before merge, use plain fences for non-Dart, and after merge confirm the firebase-hosting-merge.yml run says success (Step 12c). main is protected — a website hotfix is a new PR, not a direct push.
  • packages/flutter_gemma_litertlm/native/litert_lm/prebuilt/ excluded from pub package (.pubignore) — end users get dylibs from GitHub Release, NOT from the pub package. Updating local prebuilts without re-uploading them is invisible to users.
  • The iOS 5e0d86b pin is obsolete — modern tags ship their own prebuilt/ios_arm64/, and build_ios.sh already defaults to the current tag SHA. Always pass the pinned SHA explicitly anyway: every script's DEFAULT_REF lags whatever release you are migrating to. The real invariant is that source and accelerator prebuilts come from one tree — see the build-native skill, which owns this.
  • bazelisk clean --expunge is NOT free — it forces a full rebuild (~25 min for one platform). Only do it when WORKSPACE patch_cmds changed; otherwise incremental rebuild.
  • Linux/Windows builds run on remote VMs — see project_gcloud_vm_workflow memory.
  • macOS dylib produced LOCALLY, not in CI — see project_macos_dylib_built_locally memory. Same for iOS.
  • Pub package size is informational, NOT a hard ceiling.pubignore already excludes prebuilts, integration tests, and notebooks. The FFI bindings + pigeon + example push each published package to ~700 KB on 0.16.x; the old <=100 KB ceiling predates 0.14.0 and no longer applies. Just confirm .pubignore still excludes the heavy dirs.

Version History

  • 451fc69 Current 2026-08-20 14:01

Same Skill Collection

.claude/skills/build-native/SKILL.md
.claude/skills/ci-status/SKILL.md
.claude/skills/review-pr/SKILL.md
.claude/skills/upgrade-genkit/SKILL.md
packages/flutter_gemma_agent/assets/skills/get-current-time/SKILL.md
packages/flutter_gemma_agent/assets/skills/interactive-map/SKILL.md
packages/flutter_gemma_agent/assets/skills/kitchen-adventure/SKILL.md
packages/flutter_gemma_agent/test/fixtures/skills/interactive-map.SKILL.md
packages/flutter_gemma_agent/test/fixtures/skills/kitchen-adventure.SKILL.md
packages/flutter_gemma_agent/assets/skills/calculate-hash/SKILL.md
packages/flutter_gemma_agent/assets/skills/create-calendar-event/SKILL.md
packages/flutter_gemma_agent/assets/skills/query-wikipedia/SKILL.md
packages/flutter_gemma_agent/assets/skills/send-email/SKILL.md
packages/flutter_gemma_agent/test/fixtures/skills/calculate-hash.SKILL.md
packages/flutter_gemma_agent/test/fixtures/skills/send-email.SKILL.md
packages/flutter_gemma_agent/assets/skills/qr-code/SKILL.md

Metadata

Files
0
Version
451fc69
Hash
4343d391
Indexed
2026-08-20 14:01

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-25 09:02
浙ICP备14020137号-1 $방문자$