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
97 changes: 42 additions & 55 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,61 @@ on:
- master
workflow_dispatch:

# Auto-tags a release when the pubspec version is new. The tag is pushed with a
# GitHub App token (not GITHUB_TOKEN, which by design would not trigger other
# workflows) so that publish.yaml fires and publishes to pub.dev via OIDC.
# The job always completes successfully, so the sync workflow (gated on Deploy
# success) keeps working even before tagging is configured.
jobs:
publish:
name: Publish to pub.dev
tag:
name: Tag release on version bump
runs-on: ubuntu-latest

# Publish only from the canonical repository owner.
# Publish/tag only from the canonical repository owner.
if: github.repository_owner == 'rees46'

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: subosito/flutter-action@v2
with:
channel: 'stable'

# Publishing self-activates: it runs only once the PUB_DEV_CREDENTIALS
# secret is set. Until then the step is skipped, so Deploy stays green
# and the sync workflow (gated on Deploy success) keeps working.
- name: Resolve publish readiness
id: pubready
# Self-activates once RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY are set.
# Until then tagging is skipped, but the job stays green.
- name: Resolve tagging readiness
id: ready
env:
PUB_DEV_CREDENTIALS: ${{ secrets.PUB_DEV_CREDENTIALS }}
APP_ID: ${{ vars.RELEASE_APP_ID }}
run: |
if [ -n "$PUB_DEV_CREDENTIALS" ]; then
if [ -n "$APP_ID" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "PUB_DEV_CREDENTIALS not set — skipping publish"
echo "RELEASE_APP_ID not set — skipping auto-tagging"
echo "ready=false" >> "$GITHUB_OUTPUT"
fi

# Decide whether this version is already on pub.dev. Package name and
# version are read from pubspec.yaml so the workflow stays brand-agnostic.
- name: Check version change
id: version_check
- name: Mint app token
id: app-token
if: steps.ready.outputs.ready == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Checkout
if: steps.ready.outputs.ready == 'true'
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}

- name: Tag if version is new
if: steps.ready.outputs.ready == 'true'
run: |
NAME=$(grep '^name:' pubspec.yaml | awk '{print $2}')
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
echo "Package: $NAME@$VERSION"
CODE=$(curl -s -o /dev/null -w '%{http_code}' \
"https://pub.dev/api/packages/$NAME/versions/$VERSION")
if [ "$CODE" = "200" ]; then
echo "$NAME@$VERSION already published, skipping"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
echo "$NAME@$VERSION not yet published"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
TAG="v$VERSION"
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" \
>/dev/null 2>&1; then
echo "$TAG already exists — nothing to do"
exit 0
fi

# Modern pub reads credentials from ~/.config/dart/pub-credentials.json.
# PUB_DEV_CREDENTIALS must hold the full contents of that file, produced
# by running `flutter pub publish` once locally with the publishing
# account. Only written when we actually intend to publish.
- name: Write pub credentials
if: >-
steps.pubready.outputs.ready == 'true' &&
steps.version_check.outputs.should_publish == 'true'
env:
PUB_DEV_CREDENTIALS: ${{ secrets.PUB_DEV_CREDENTIALS }}
run: |
mkdir -p ~/.config/dart
printf '%s' "$PUB_DEV_CREDENTIALS" > ~/.config/dart/pub-credentials.json

# Publish whenever pubspec carries a version not yet on pub.dev.
# Pushes that don't bump the version are skipped by version_check above.
- name: Publish to pub.dev
if: >-
steps.pubready.outputs.ready == 'true' &&
steps.version_check.outputs.should_publish == 'true'
run: flutter pub publish --force
git config user.name "github-actions[bot]"
git config user.email \
"41898282+github-actions[bot]@users.noreply.github.com"
git tag "$TAG"
git push origin "$TAG"
echo "Pushed $TAG"
51 changes: 51 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish

# pub.dev automated publishing is OIDC-based and only allowed when the workflow
# is triggered by pushing a git tag (see deploy.yaml for the auto-tagger).
# No long-lived secret is needed — the owner authorizes this repository once on
# pub.dev (Admin -> Automated publishing, tag pattern v{{version}}).
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'

jobs:
publish:
name: Publish to pub.dev
runs-on: ubuntu-latest

# Publish only from the canonical repository owner.
if: github.repository_owner == 'rees46'

permissions:
id-token: write # OIDC authentication to pub.dev

steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: 'stable'

- name: Get dependencies
run: flutter pub get

# Guard so a tag on an already-published version is a no-op success
# (e.g. right after the manual first publish that reserved the name).
- name: Check if already published
id: check
run: |
NAME=$(grep '^name:' pubspec.yaml | awk '{print $2}')
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
CODE=$(curl -s -o /dev/null -w '%{http_code}' \
"https://pub.dev/api/packages/$NAME/versions/$VERSION")
if [ "$CODE" = "200" ]; then
echo "$NAME@$VERSION already published — skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish
if: steps.check.outputs.skip != 'true'
run: flutter pub publish --force
70 changes: 35 additions & 35 deletions docs/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
# Публикация на pub.dev

Пакет `rees46_sdk` публикуется на pub.dev из этого
репозитория (`rees46/flutter-sdk`).
Пакет `rees46_sdk` публикуется на pub.dev из этого репозитория
(`rees46/flutter-sdk`).

Схема — **token-based на push в master**: merge в master → `Deploy` →
publish.
Схема — **OIDC (automated publishing)**: pub.dev доверяет этому GitHub-репо,
долгоживущие секреты/токены публикации не нужны. Официальная дока:
<https://dart.dev/tools/pub/automated-publishing>.

## Один раз: аккаунты и первый релиз
Ключевое из доки: публикация по OIDC разрешена, только когда workflow запущен
**push'ем git-тега**, и автопубликацию можно включить лишь для **уже
существующего** пакета (первую версию заливают вручную).

1. (Рекомендуется) Создать verified publisher `rees46.com` и сделать его
владельцем пакета. pub.dev → Create publisher → подтверждение домена
через DNS TXT.
2. Застолбить имя первым ручным релизом (автопубликацию pub.dev можно
включить только для уже существующего пакета):
## Один раз: со стороны владельца pub.dev-аккаунта

1. (По желанию) Verified publisher: подтвердить домен `rees46.com` в Google
Search Console (DNS TXT) и создать publisher на pub.dev. Для самой
публикации не обязательно. См. <https://dart.dev/tools/pub/verified-publishers>.
2. Застолбить имя первым ручным релизом (локальный логин, ничего никому не
передаётся):
```bash
flutter pub publish --dry-run # 0 ошибок по метаданным
flutter pub publish # интерактивный логин Google
flutter pub publish # создаст пакет rees46_sdk
```
`example/` не публикуется (`publish_to: none`).
3. Включить автопубликацию: pub.dev → пакет → Admin → Automated publishing →
GitHub Actions; **Repository:** `rees46/flutter-sdk`; **Tag pattern:**
`v{{version}}`.

## Настройка CI

1. Локально получить credentials публикующего аккаунта: один раз
`flutter pub publish` → файл `~/.config/dart/pub-credentials.json`.
2. Содержимое файла → **Secret** `PUB_DEV_CREDENTIALS`.
## Один раз: со стороны репозитория (CI)

`deploy.yaml` на каждый push в master:
- **самовключение**: публикация выполняется только если задан секрет
`PUB_DEV_CREDENTIALS`. Пока его нет — publish-шаг скипается, Deploy остаётся
зелёным, и sync (завязанный на успех Deploy) продолжает работать;
- читает имя и версию из `pubspec.yaml`;
- проверяет через pub.dev API, не опубликована ли уже эта версия;
- если версия новая (и секрет задан) — пишет credentials в
`~/.config/dart/pub-credentials.json` и публикует `flutter pub publish --force`.
Пуши без бампа версии — скип.
Автотегировщику (`deploy.yaml`) нужен токен GitHub App для пуша тега — тег от
`GITHUB_TOKEN` по дизайну не триггерит другой workflow. Задать в репозитории:
- **Variable** `RELEASE_APP_ID`
- **Secret** `RELEASE_APP_PRIVATE_KEY`

## Релиз
(источник — versioner-app). Пока они не заданы, автотегирование скипается, а
`Deploy` остаётся зелёным (sync продолжает работать).

1. Поднять `version:` в `pubspec.yaml`, обновить `CHANGELOG.md`.
2. PR → merge в master.
3. `Deploy` видит новую версию (нет на pub.dev) → публикует пакет.
## Как работает релиз

## Альтернатива: OIDC (на будущее)
1. Поднять `version:` в `pubspec.yaml`, обновить `CHANGELOG.md`, смержить в
master.
2. `Deploy` (`deploy.yaml`) видит новую версию → создаёт и пушит тег
`v<version>` app-токеном.
3. Тег запускает `publish.yaml` → `flutter pub publish --force` по OIDC.
Если версия уже на pub.dev — шаг пропускается (no-op).

Безопаснее (без долгоживущих секретов), но требует релизов по git-тегам:
pub.dev → пакет → Admin → Automated publishing → GitHub Actions, репозиторий +
tag pattern `v{{version}}`; в CI — workflow с `permissions: id-token: write` и
триггером `on: push: tags`.
Тег всегда выводится из версии в pubspec, поэтому версия и тег синхронизированы
по построению; pub.dev дополнительно проверяет их совпадение.
Loading