From c009fd9779449f4ffacc98d462a9a0da0a9d2181 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Tue, 13 Oct 2026 12:39:10 -0400 Subject: [PATCH 1/4] docs: add RELEASING.md covering the CI publish workflow --- RELEASING.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..185fb14 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,69 @@ +# Releasing + +This repository is a monorepo of independently versioned packages published to [pub.dev](https://pub.dev). Publishing runs in CI (`.github/workflows/publish.yml`) using pub.dev [automated publishing](https://dart.dev/tools/pub/automated-publishing) (OIDC, no stored credentials). Nobody runs `dart pub publish` from a laptop. + +## 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. **Publish.** Core: publish a GitHub Release on `main` with tag `segment_analytics-vX.Y.Z`. That fires `publish.yml`, which always publishes `packages/core`; the tag does not select the package. A plugin: run the **Publish to pub.dev** workflow manually (`workflow_dispatch`) with `package` set to its path under `packages/`, e.g. `plugins/plugin_firebase`. +4. **Approve** the `publish` job if the `production` environment requires review. The workflow runs the package's tests first and publishes only if they pass. +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 publishing a plugin that needs it. + +## Tags + +Use `-v` (`segment_analytics-v1.2.0`, `segment_analytics_plugin_firebase-v1.0.2`). Packages version independently, so a bare `vX.Y.Z` is ambiguous. The tag pattern configured under each package's **Admin → Automated publishing** on pub.dev must match this format. + +## One-time setup per package (pub.dev) + +On each package's **Admin** tab, enable automated publishing from GitHub Actions for repository `segmentio/analytics_flutter` with the tag pattern above. A package without this fails at the publish step 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 publisher can run `dart pub publish` locally from the package directory after `dart pub login`. Record the release in the CHANGELOG as usual and tag it afterwards. From 1c2ae9c9c7316cc141c0b7241150a0b2ca529d61 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 16 Oct 2026 11:33:56 -0400 Subject: [PATCH 2/4] docs: spell out pub.dev automated-publishing settings in RELEASING.md --- RELEASING.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index 185fb14..349c9e9 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -52,7 +52,14 @@ Use `-v` (`segment_analytics-v1.2.0`, `segment_analytics_plugi ## One-time setup per package (pub.dev) -On each package's **Admin** tab, enable automated publishing from GitHub Actions for repository `segmentio/analytics_flutter` with the tag pattern above. A package without this fails at the publish step with an authorization error. +On each 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}}` (see Tags above) +- **`workflow_dispatch`:** enabled, so plugins can be released from the manual trigger +- **Environment:** `production`, exactly as named in `publish.yml`. A mismatch fails every publish with an authorization error. + +A package without this fails at the publish step with an authorization error. ## Changing the Flutter version From b7cfaf182f8067d610c4e402dcc74f51c1577a25 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 16 Oct 2026 11:39:19 -0400 Subject: [PATCH 3/4] ci: check release metadata before publishing --- .github/scripts/check-release-metadata.sh | 38 +++++++++++++++++++++++ .github/workflows/publish.yml | 7 +++++ 2 files changed, 45 insertions(+) create mode 100755 .github/scripts/check-release-metadata.sh diff --git a/.github/scripts/check-release-metadata.sh b/.github/scripts/check-release-metadata.sh new file mode 100755 index 0000000..74fa85c --- /dev/null +++ b/.github/scripts/check-release-metadata.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Fails the publish run before anyone is asked to approve it if the package about +# to be published is inconsistent: pubspec, changelog, version.dart and (for a +# release trigger) the tag must all agree on the version. +# +# Inputs (env): PACKAGE path under packages/ (e.g. core, plugins/plugin_firebase) +# EVENT github.event_name +# TAG github.ref_name, only read for the release event +set -euo pipefail + +fail() { echo "::error::$*"; exit 1; } + +# 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, regardless of the tag. +if [[ "$EVENT" == "release" ]]; then + expected="$name-v$version" + [[ "$TAG" == "$expected" ]] || fail "release tag '$TAG' != '$expected' (a release event always publishes packages/core; publish plugins with workflow_dispatch)" +fi + +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" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fe524a7..5bf1493 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,6 +27,13 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Check release metadata + run: .github/scripts/check-release-metadata.sh + env: + PACKAGE: ${{ github.event.inputs.package || 'core' }} + EVENT: ${{ github.event_name }} + TAG: ${{ github.ref_name }} + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: distribution: 'zulu' From 5ccf915c85cdd79b0bca3235d58912241da6ba34 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 16 Oct 2026 11:53:34 -0400 Subject: [PATCH 4/4] ci: publish on tag push and resolve the package from the tag --- .github/scripts/check-release-metadata.sh | 38 ++++++++++++++------ .github/workflows/publish.yml | 42 +++++++++++++++++------ RELEASING.md | 37 +++++++++++++------- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/.github/scripts/check-release-metadata.sh b/.github/scripts/check-release-metadata.sh index 74fa85c..6e5221e 100755 --- a/.github/scripts/check-release-metadata.sh +++ b/.github/scripts/check-release-metadata.sh @@ -1,18 +1,30 @@ #!/usr/bin/env bash -# Fails the publish run before anyone is asked to approve it if the package about -# to be published is inconsistent: pubspec, changelog, version.dart and (for a -# release trigger) the tag must all agree on the version. +# 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): PACKAGE path under packages/ (e.g. core, plugins/plugin_firebase) -# EVENT github.event_name -# TAG github.ref_name, only read for the release event +# 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'" +[[ "${PACKAGE:-}" =~ ^(core|plugins/plugin_[a-z_]+)$ ]] || fail "invalid package '${PACKAGE:-}'" dir="packages/$PACKAGE" [[ -f "$dir/pubspec.yaml" ]] || fail "$dir/pubspec.yaml not found" @@ -20,12 +32,15 @@ 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, regardless of the tag. -if [[ "$EVENT" == "release" ]]; then - expected="$name-v$version" - [[ "$TAG" == "$expected" ]] || fail "release tag '$TAG' != '$expected' (a release event always publishes packages/core; publish plugins with workflow_dispatch)" +# 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'" @@ -36,3 +51,4 @@ if [[ "$PACKAGE" == "core" ]]; then 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 5bf1493..b6aa061 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,19 +26,32 @@ env: FLUTTER_VERSION: "3.29.2" jobs: - test: - name: Pre-publish tests + 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: - PACKAGE: ${{ github.event.inputs.package || 'core' }} 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 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: @@ -50,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: @@ -73,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 index 349c9e9..b7e3ad7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,6 +1,6 @@ # Releasing -This repository is a monorepo of independently versioned packages published to [pub.dev](https://pub.dev). Publishing runs in CI (`.github/workflows/publish.yml`) using pub.dev [automated publishing](https://dart.dev/tools/pub/automated-publishing) (OIDC, no stored credentials). Nobody runs `dart pub publish` from a laptop. +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 @@ -40,26 +40,37 @@ Plugins depend on core through a pub.dev constraint (`segment_analytics: ^1.x`), dart pub publish --dry-run ``` -3. **Publish.** Core: publish a GitHub Release on `main` with tag `segment_analytics-vX.Y.Z`. That fires `publish.yml`, which always publishes `packages/core`; the tag does not select the package. A plugin: run the **Publish to pub.dev** workflow manually (`workflow_dispatch`) with `package` set to its path under `packages/`, e.g. `plugins/plugin_firebase`. -4. **Approve** the `publish` job if the `production` environment requires review. The workflow runs the package's tests first and publishes only if they pass. +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 publishing a plugin that needs 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. -## Tags +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. -Use `-v` (`segment_analytics-v1.2.0`, `segment_analytics_plugin_firebase-v1.0.2`). Packages version independently, so a bare `vX.Y.Z` is ambiguous. The tag pattern configured under each package's **Admin → Automated publishing** on pub.dev must match this format. +## One-time setup -## One-time setup per package (pub.dev) +**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. -On each package's **Admin** tab, under **Automated publishing**, choose **Publishing from GitHub Actions** (not the Google Cloud service account option) and set: +**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}}` (see Tags above) -- **`workflow_dispatch`:** enabled, so plugins can be released from the manual trigger -- **Environment:** `production`, exactly as named in `publish.yml`. A mismatch fails every publish with an authorization error. +- **Tag pattern:** `-v{{version}}`, matching the tag above +- **Push** and **`workflow_dispatch`:** both enabled +- **Environment:** `production`, exactly as named in `publish.yml` -A package without this fails at the publish step with an authorization error. +Without this the publish step fails with an authorization error. ## Changing the Flutter version @@ -73,4 +84,4 @@ Take the checksum for the `stable/linux/flutter_linux_-stable.tar.xz` a ## Break-glass -If automated publishing is unavailable, a pub.dev publisher can run `dart pub publish` locally from the package directory after `dart pub login`. Record the release in the CHANGELOG as usual and tag it afterwards. +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.