diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index b707489..31b8ef1 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -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" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..b131182 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -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 diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 00d7e36..a44ee23 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -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-репо, +долгоживущие секреты/токены публикации не нужны. Официальная дока: +. -## Один раз: аккаунты и первый релиз +Ключевое из доки: публикация по 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. Для самой + публикации не обязательно. См. . +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` 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 дополнительно проверяет их совпадение.