Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/scripts/check-release-metadata.sh
Original file line number Diff line number Diff line change
@@ -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=<path under packages/>`
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: <pubspec name>-v<version>.
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}"
43 changes: 35 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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_<name>)"
required: true
default: "core"

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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 }}
87 changes: 87 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -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 `<pubspec name>-v<version>`, 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 <package>` 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_<name>`. 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:** `<package>-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_<version>-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.
Loading