diff --git a/.github/scripts/check-release-metadata.sh b/.github/scripts/check-release-metadata.sh
new file mode 100755
index 0000000..6e5221e
--- /dev/null
+++ b/.github/scripts/check-release-metadata.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# Resolves which package a publish run targets and fails the run, before anyone
+# is asked to approve it, if that package is inconsistent: pubspec, changelog,
+# version.dart and the tag must all agree on the version.
+#
+# Inputs (env): EVENT github.event_name (push of a tag, or workflow_dispatch)
+# TAG github.ref_name, read only for push
+# PACKAGE workflow_dispatch input: path under packages/
+# GITHUB_OUTPUT receives `package=`
+set -euo pipefail
+
+fail() { echo "::error::$*"; exit 1; }
+
+if [[ "$EVENT" == "push" ]]; then
+ # pub.dev only accepts tag pushes for push-triggered publishing, and the tag
+ # is the sole selector of the package: -v.
+ name="${TAG%%-v[0-9]*}"
+ case "$name" in
+ segment_analytics) PACKAGE=core ;;
+ segment_analytics_plugin_[a-z_]*) PACKAGE="plugins/plugin_${name#segment_analytics_plugin_}" ;;
+ *) fail "tag '$TAG' does not name a package in this repo" ;;
+ esac
+fi
+
+# PACKAGE reaches working-directory in the workflow; reject anything that could
+# escape packages/.
+[[ "${PACKAGE:-}" =~ ^(core|plugins/plugin_[a-z_]+)$ ]] || fail "invalid package '${PACKAGE:-}'"
+dir="packages/$PACKAGE"
+[[ -f "$dir/pubspec.yaml" ]] || fail "$dir/pubspec.yaml not found"
+
+name=$(sed -n 's/^name: *//p' "$dir/pubspec.yaml" | head -1)
+version=$(sed -n 's/^version: *//p' "$dir/pubspec.yaml" | head -1)
+[[ -n "$name" && -n "$version" ]] || fail "could not read name/version from $dir/pubspec.yaml"
+
+# pub.dev publishes whatever pubspec.yaml says, so the tag must say the same.
+if [[ "$EVENT" == "push" ]]; then
+ [[ "$TAG" == "$name-v$version" ]] || fail "tag '$TAG' != '$name-v$version' (pubspec.yaml in $dir)"
+fi
+
+# Whatever triggered the run, the code being published must be on main.
+git fetch -q origin main
+git merge-base --is-ancestor "$GITHUB_SHA" origin/main || fail "$GITHUB_SHA is not on main"
+
+top=$(sed -n 's/^## *//p' "$dir/CHANGELOG.md" | head -1)
+[[ "$top" == "$version" ]] || fail "newest CHANGELOG section is '$top', pubspec version is '$version'"
+
+if [[ "$PACKAGE" == "core" ]]; then
+ # segmentVersion is sent as context.library.version on every event.
+ dart_version=$(sed -n 's/^const segmentVersion = "\(.*\)";/\1/p' "$dir/lib/version.dart")
+ [[ "$dart_version" == "$version" ]] || fail "lib/version.dart is '$dart_version', pubspec version is '$version'"
+fi
+
+echo "OK: $name $version"
+echo "package=$PACKAGE" >> "${GITHUB_OUTPUT:-/dev/null}"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6d5b717..1a32cc1 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,12 +1,18 @@
name: Publish to pub.dev
+# pub.dev automated publishing accepts only tag pushes and workflow_dispatch; a
+# `release` event is rejected. The tag selects the package (see
+# .github/scripts/check-release-metadata.sh). workflow_dispatch is for re-running
+# a publish whose tag-triggered run failed.
on:
- release:
- types: [published]
+ push:
+ tags:
+ - 'segment_analytics-v[0-9]+.[0-9]+.[0-9]+*'
+ - 'segment_analytics_plugin_*-v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
inputs:
package:
- description: "Package to publish (e.g. core, plugins/plugin_firebase)"
+ description: "Package to publish (core, or plugins/plugin_)"
required: true
default: "core"
@@ -20,9 +26,29 @@ env:
FLUTTER_VERSION: "3.38.0"
jobs:
+ meta:
+ name: Resolve package and check release metadata
+ runs-on: ubuntu-latest-large
+ outputs:
+ package: ${{ steps.check.outputs.package }}
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ fetch-depth: 0
+
+ - name: Check release metadata
+ id: check
+ run: .github/scripts/check-release-metadata.sh
+ env:
+ EVENT: ${{ github.event_name }}
+ TAG: ${{ github.ref_name }}
+ PACKAGE: ${{ github.event.inputs.package }}
+
test:
name: Pre-publish tests
runs-on: ubuntu-latest-large
+ needs: [meta]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -43,16 +69,17 @@ jobs:
- name: Get dependencies
run: flutter pub get
- working-directory: packages/${{ github.event.inputs.package || 'core' }}
+ working-directory: packages/${{ needs.meta.outputs.package }}
- name: Run tests
- run: flutter test
- working-directory: packages/${{ github.event.inputs.package || 'core' }}
+ run: |
+ if [ -d test ]; then flutter test; else echo "no test/ directory, skipping"; fi
+ working-directory: packages/${{ needs.meta.outputs.package }}
publish:
name: Publish to pub.dev
runs-on: ubuntu-latest-large
- needs: [test]
+ needs: [meta, test]
environment: production
steps:
@@ -66,4 +93,4 @@ jobs:
- name: Publish to pub.dev (Trusted Publishing)
run: dart pub publish --force
- working-directory: packages/${{ github.event.inputs.package || 'core' }}
+ working-directory: packages/${{ needs.meta.outputs.package }}
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..b7e3ad7
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,87 @@
+# Releasing
+
+This repository is a monorepo of independently versioned packages published to [pub.dev](https://pub.dev). Pushing a package-qualified tag publishes that package from CI (`.github/workflows/publish.yml`) using pub.dev [automated publishing](https://dart.dev/tools/pub/automated-publishing) (OIDC, no stored credentials).
+
+## Packages
+
+| Package | Path |
+|---------|------|
+| `segment_analytics` (core) | `packages/core` |
+| `segment_analytics_plugin_adjust` | `packages/plugins/plugin_adjust` |
+| `segment_analytics_plugin_advertising_id` | `packages/plugins/plugin_advertising_id` |
+| `segment_analytics_plugin_amplitude` | `packages/plugins/plugin_amplitude` |
+| `segment_analytics_plugin_appsflyer` | `packages/plugins/plugin_appsflyer` |
+| `segment_analytics_plugin_firebase` | `packages/plugins/plugin_firebase` |
+| `segment_analytics_plugin_idfa` | `packages/plugins/plugin_idfa` |
+
+Plugins depend on core through a pub.dev constraint (`segment_analytics: ^1.x`), not a path dependency, so a plugin can only be published against a core version that is already on pub.dev. **Release core first.**
+
+## Versioning
+
+[SemVer](https://semver.org/) per package; commits follow [Conventional Commits](CONTRIBUTING.md#commit-message-convention). Versions are bumped by hand in the release PR.
+
+- A raised minimum Flutter or Dart SDK constraint is a breaking change for every consumer, including Android and web, because pub resolves it for the whole package. Bump the major version.
+- Core's version lives in two files that must match, or events report the wrong `context.library.version`:
+ - `packages/core/pubspec.yaml` → `version:`
+ - `packages/core/lib/version.dart` → `segmentVersion`
+- A plugin's version lives only in its own `pubspec.yaml`.
+- When core ships a new major, every plugin's `segment_analytics:` constraint stops matching it. Widen the constraint (e.g. `">=1.1.1 <3.0.0"`) and release the plugins after core.
+
+## Release steps
+
+1. **Release PR.** Bump the version(s), add a `## X.Y.Z` section at the top of each package's `CHANGELOG.md`, and update the README if compatibility changes. Merge to `main` with CI green.
+2. **Dry run** from the package directory:
+
+ ```bash
+ cd packages/core # or the plugin being released
+ flutter pub get
+ flutter analyze
+ flutter test
+ dart pub publish --dry-run
+ ```
+
+3. **Tag and push** from `main` at the merged release commit:
+
+ ```bash
+ git tag segment_analytics-vX.Y.Z
+ git push origin segment_analytics-vX.Y.Z
+ ```
+
+ The tag is `-v`, e.g. `segment_analytics_plugin_firebase-v1.0.2` for a plugin. The tag alone selects the package.
+4. **Approve** the `publish` job when it pauses on the `production` environment. Before that, the workflow checks that the tag, `pubspec.yaml`, the newest `CHANGELOG.md` section and (for core) `lib/version.dart` agree, that the commit is on `main`, and that the package's tests pass. A failure there publishes nothing.
+5. **Verify** the version appears on the package's pub.dev page and that a fresh `flutter pub add ` resolves it.
+
+When releasing several packages, wait for core to appear on pub.dev before tagging a plugin that needs it.
+
+## Re-running a failed publish
+
+If the tag-triggered run failed for a transient reason, re-run it from the Actions tab. If the tag points at the wrong commit, fix `main`, then delete and recreate the tag.
+
+The **Publish to pub.dev** workflow can also be started by hand (`workflow_dispatch`) with `package` set to `core` or `plugins/plugin_`. It runs the same checks. Do not tag afterwards: the tag would start a second publish of a version that already exists, which fails.
+
+## One-time setup
+
+**GitHub:** the `production` environment (Settings → Environments) needs required reviewers, and its deployment branches and tags limited to `main` and the tag patterns `segment_analytics-v*` and `segment_analytics_plugin_*-v*`. The pub.dev environment requirement below only checks that the job names the environment; the reviewers are what make it a gate.
+
+**pub.dev, per package:** on the package's **Admin** tab, under **Automated publishing**, choose **Publishing from GitHub Actions** (not the Google Cloud service account option) and set:
+
+- **Repository:** `segmentio/analytics_flutter`
+- **Tag pattern:** `-v{{version}}`, matching the tag above
+- **Push** and **`workflow_dispatch`:** both enabled
+- **Environment:** `production`, exactly as named in `publish.yml`
+
+Without this the publish step fails with an authorization error.
+
+## Changing the Flutter version
+
+The Flutter SDK is pinned and checksum-verified. To bump it, change all of these together:
+
+- `.github/actions/setup-flutter/action.yml` → default `flutter-version`
+- `FLUTTER_VERSION` in `.github/workflows/ci.yml` and `.github/workflows/publish.yml`
+- the `sha256:` input on every `setup-flutter` step in both workflows (one in `ci.yml`, two in `publish.yml`)
+
+Take the checksum for the `stable/linux/flutter_linux_-stable.tar.xz` archive from `https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json`.
+
+## Break-glass
+
+If automated publishing is unavailable, a pub.dev uploader can run `dart pub publish` locally from the package directory after `dart pub login`. Tag the release afterwards, noting that pushing the tag starts the workflow, which will fail on the already-published version; that is harmless.