From 305a594f850ac87322bc980a69d76b178a98dd88 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 12 Sep 2026 21:40:07 +0900 Subject: [PATCH 1/2] =?UTF-8?q?ci:=20=E5=85=AC=E9=96=8B=E3=82=92=20main=20?= =?UTF-8?q?=E3=81=AE=20CI=20=E3=81=AE=E6=88=90=E5=8A=9F=E3=81=8B=E3=82=89?= =?UTF-8?q?=E8=B5=B7=E3=81=93=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 版宣言 PR のマージ後、main の CI が green になった時点を公開の起点とする。 公開側で同じ範囲のテストを走らせ直していたゲートは落とし、テストの実行は PR と main のそれぞれ 1 回に収める。版更新 PR への CI のブランチ指定起動も やめ、承認待ちで止まっているラン自体を起こす形に寄せる。 Gradle Plugin Portal への公開は、Maven Central の成果物が repo1 から引ける ことを確かめてから行う。公開したプラグインは利用者のビルドでコンパイラ プラグインを Central から解決するため、先に Portal へ出すと解決できない版が 並ぶ。Central は公開の確定まで自動で行い、待つのは repo1 への同期だけとする。 各ジョブは公開先の状態を見て済んでいれば飛ばすため、失敗したジョブの再実行 だけで公開を完了できる。GitHub Release・API ドキュメント・次版の PR は 両方の公開先へ出た後に置き、入手できない版が記録に立たないようにする。 main の CI はコミット毎の結果が要るため、後続の push では取り消さない。 Co-Authored-By: Claude Opus 5 --- .github/workflows/_publish.yml | 233 -------------------- .github/workflows/ci.yml | 8 +- .github/workflows/publish.yml | 376 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 83 +------- 4 files changed, 390 insertions(+), 310 deletions(-) delete mode 100644 .github/workflows/_publish.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml deleted file mode 100644 index 084e7a8..0000000 --- a/.github/workflows/_publish.yml +++ /dev/null @@ -1,233 +0,0 @@ -# CD。タグ v-<自版> を ref に取り、ルートビルドと integration-test の green を -# 確認してから Maven Central と Gradle Plugin Portal へ公開する。GitHub Release は公開が通った -# 記録として最後に作る。タグ付けと起動は release.yml が版宣言のマージ時に行う(GITHUB_TOKEN が -# 押したタグの push はワークフローを起こさないため、起点を workflow_dispatch に置く)。 -# 名前の接頭辞 `_` は手で起こさないワークフローの印であり、リリースの入口は release.yml が持つ。 -# ランナーは macos 単騎(宣言済みの全ターゲットをネイティブ + クロスコンパイルで賄えるため)。 -# アクションはコミットハッシュで固定する(末尾コメントの vX.Y.Z は参照用。更新は Dependabot が追随する) -name: _publish (internal) - -on: - # ref はタグ。別の ref で起こしても、下の版照合が食い違いとして落とす - workflow_dispatch: - -permissions: - contents: read - -# 公開は取り消せないため、走行中のランをキャンセルしない -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - publish: - runs-on: macos-latest - timeout-minutes: 180 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.konan - key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} - - # 版の正はビルド(カタログの Kotlin 版 + gradle.properties の enumizerVersion)であり、Release の - # タグはその写しに過ぎない。両者の一致をここで確かめる。手で打たれたタグもこの照合で落ちる - - name: Verify tag matches derived version - run: | - set -euo pipefail - version=$(./gradlew -q properties --property version | sed -n 's/^version: //p') - if [ "v$version" != "$GITHUB_REF_NAME" ]; then - echo "::error::tag $GITHUB_REF_NAME does not match derived version $version" - exit 1 - fi - - # ランナーが宣言ターゲットを全て賄えることを先に確かめる。賄えないターゲットの publication は - # kotlin.native.ignoreDisabledTargets により静かに落ち、成果物が欠けたまま公開が成功してしまう。 - # 実行時の担保は publish タスクの依存としても掛かっており、ここは長いゲートの前で落とすための前倒し - - name: Verify all declared targets are publishable - run: ./gradlew :sealed-class-enumizer-runtime-api:checkPublishableTargets - - # 公開前のゲート。CI と同じ範囲を 1 ジョブで通す - - run: ./gradlew build - - run: >- - ./gradlew -p integration-test build - -x :gradle-integration:test -x :maven-integration:test - # 重いテストはローカル Maven の非タイムスタンプ SNAPSHOT を入力に持ち UP-TO-DATE 判定が - # 信用できないため --rerun で必ず再実行する(ci.yml と同じ理由) - - run: ./gradlew -p integration-test :gradle-integration:test --rerun - - run: ./gradlew -p integration-test :maven-integration:test --rerun - - # Central Portal へはデプロイの作成までとし、公開の確定は Portal 上の validation 確認後に手で行う - # (publishAndReleaseToMavenCentral は使わない)。vanniktech の設定値は ORG_GRADLE_PROJECT_ 接頭辞の - # 環境変数で渡す(Gradle プロパティ名は大小文字を区別するため、シークレット名とは別に対応付ける) - - name: Publish to Maven Central - env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} - run: ./gradlew publishToMavenCentral - - # Plugin Portal は取り消しができないため Central の後に置く - - name: Publish to Gradle Plugin Portal - env: - GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} - GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} - run: ./gradlew :sealed-class-enumizer-gradle-plugin:publishPlugins - - # 公開の記録として GitHub Release を作る。公開の後に作るため、成果物が無いまま Latest release が - # 立つことはない。本文に変更点は持たず CHANGELOG の当該節を指す(変更点の正は CHANGELOG であり、 - # 写しを置くと版を跨いで食い違う)。Full Changelog 行だけは GitHub の自動生成と同じものを API から取る - github-release: - needs: publish - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - steps: - # 作業ツリーは要らない。gh は GH_REPO で対象を取る - - name: Create the GitHub Release - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - if gh release view "$TAG" >/dev/null 2>&1; then - echo "::notice::a release for $TAG already exists" - exit 0 - fi - # CHANGELOG の版見出し `## [<版>]` に付くアンカー。記号が落ちた形になる - anchor=$(printf '%s' "${TAG#v}" | tr -d .) - full=$(gh api "repos/$GH_REPO/releases/generate-notes" -f tag_name="$TAG" --jq .body \ - | grep '^\*\*Full Changelog\*\*:') - gh release create "$TAG" --verify-tag --title "$TAG" --notes "$(printf '%s\n\n%s\n' \ - "Changes in this release: [CHANGELOG](https://github.com/$GH_REPO/blob/$TAG/CHANGELOG.md#$anchor)" \ - "$full")" - - # API ドキュメントの掲載。Pages のソースは Actions のため、公開されるのはこのジョブが上げる成果物であり、 - # 保管ブランチ(gh-pages)の内容が直接配信されることはない。同ブランチは過去版の出力だけを - # versions/<版>/ の形で持ち、版切替 UI の入力として使う。deploy はサイト全体を置き換えるため、 - # 過去版の保持はこの保管場所から毎回作り直すことで成り立つ - docs: - needs: publish - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - contents: write - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - - # 初回リリース時は保管ブランチがまだ無い。その場合は空の履歴から始める - - name: Restore archived versions - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - url="https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" - if git ls-remote --exit-code --heads "$url" gh-pages >/dev/null 2>&1; then - git clone --branch gh-pages --depth 1 "$url" build/gh-pages - else - mkdir -p build/gh-pages - git -C build/gh-pages init -b gh-pages -q - git -C build/gh-pages remote add origin "$url" - fi - mkdir -p build/gh-pages/versions - - - run: ./gradlew dokkaGenerate -PolderDocsDir=build/gh-pages/versions - - # 保管するのは各版の出力そのものとし、生成物に含まれる過去版(older/)は落とす。 - # 落とさないと版を重ねるたびに過去版が入れ子で積み上がる - - name: Archive this version - run: | - set -euo pipefail - dest="build/gh-pages/versions/${GITHUB_REF_NAME#v}" - rm -rf "$dest" - mkdir -p "$dest" - cp -r build/dokka/html/. "$dest/" - rm -rf "$dest/older" - - # 保管ブランチの更新は通常のコミットで行う(force push は保護設定と整合しない) - - name: Update archive branch - working-directory: build/gh-pages - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --staged --quiet; then - echo "::notice::archive is already up to date" - else - git commit -q -m "docs: ${GITHUB_REF_NAME#v} の API ドキュメントを保管する" - git push origin gh-pages - fi - - - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - with: - path: build/dokka/html - - id: deploy - uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 - - # 公開した版は消費済みのため、開発版(次パッチの -SNAPSHOT)へ戻す PR を出す。 - # 直接 push しないのは、既定ブランチが PR 経由の更新を必須としているため。 - # 配布物を説明する文書の版表記はここでは変わらない(同期の対象はリリース版を宣言している間に限るため、 - # 公開済みの版を指したまま据え置かれる) - next-version: - needs: publish - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - pull-requests: write - # GITHUB_TOKEN が作った PR には必須チェックが報告されないため、CI をブランチ指定で起こす - actions: write - steps: - # タグではなく既定ブランチを起点にする(タグの指す位置が既定ブランチの先端とは限らない) - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: main - - name: Open version bump pull request - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - current=$(sed -n 's/^enumizerVersion=//p' gradle.properties) - case "$current" in - *-SNAPSHOT) - echo "::notice::$current is already a development version" - exit 0 - ;; - esac - IFS=. read -r major minor patch <<< "$current" - next="$major.$minor.$((patch + 1))-SNAPSHOT" - branch="chore/next-version-$next" - sed -i "s/^enumizerVersion=.*/enumizerVersion=$next/" gradle.properties - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git switch -c "$branch" - git commit -q -am "build: 開発版を $next へ進める" - git push -q origin "$branch" - gh pr create --base main --head "$branch" \ - --title "build: 開発版を $next へ進める" \ - --body "$(printf '%s\n' \ - "${GITHUB_REF_NAME#v} の公開に伴い、開発版を次のパッチへ進める。" \ - "" \ - "配布物を説明する文書の版表記は公開済みの版を指したまま据え置かれる。" \ - "次のリリースで -SNAPSHOT を外す際にまとめて追随する。")" - # チェックはコミットの SHA へ紐付くため、この起動で PR 側の必須チェックが満たされる - gh workflow run ci.yml --ref "$branch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 189de1a..da26505 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,17 +34,17 @@ on: - ".github/workflows/ci.yml" - "!**.md" pull_request: - # GITHUB_TOKEN が作った PR は push / pull_request を起こさないため、release / _publish が出す - # 版更新の PR には必須チェックが報告されない。それらがブランチを指定して起こすための入口 - # (workflow_dispatch は GITHUB_TOKEN でも起動できる)。手での起動口も兼ねる + # ブランチを指定して手で回すための入口 workflow_dispatch: permissions: contents: read +# main の push はコミット毎に結果が要る(後続の push で取り消すと、そのコミットを通した記録が残らない)。 +# PR は最新の push の結果だけが要るため取り消す concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: # PR でのビルド入力の変更有無を判定する(push は起動条件の paths で判定済みのため常に true とする)。 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5ff4d1b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,376 @@ +# CD。main の CI が green になった時点を起点に、版宣言の入ったコミットへタグを付けて公開する。 +# 公開の順は Maven Central → Gradle Plugin Portal。後者は前者の成果物が実際に引けるようになるまで待つ +# (公開したプラグインは利用者のビルドでコンパイラプラグインを Maven Central から解決するため、 +# 先に Portal へ出すと解決できない版が並ぶ)。Central は公開の確定まで自動で行うため、 +# 待つのは repo1 への同期だけであり、リリースに手作業は残らない。 +# 公開してよいかを決めるのは CI の結果であり、ここでテストは走らせない(同じ検証の二重実行を避ける)。 +# 各ジョブは公開先の状態を見て「済んでいれば飛ばす」ため、失敗したジョブの再実行だけで公開を完了できる。 +# ランナーは Central への公開のみ macos(宣言済みの全ターゲットをネイティブ + クロスコンパイルで賄えるため)。 +# アクションはコミットハッシュで固定する(末尾コメントの vX.Y.Z は参照用。更新は Dependabot が追随する) +name: publish (auto) + +# 起点は CI の完了。参照はワークフロー名で行うため、ci.yml の name を変えるならここも変える。 +# 手での起動口は持たない(公開してよいかを決めるのはテストの結果であり、起こし手ではない)。 +# CI が落ちた場合は、直して main の CI を green にすれば改めてここが起きる +on: + workflow_run: + workflows: [CI] + types: [completed] + branches: [main] + +permissions: + contents: read + +# 公開は取り消せないため、走行中のランをキャンセルしない。公開の途中で別のランが起きても、 +# 直列化により二重の公開にはならない +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + # 公開する版と公開先を決め、タグを付ける。版宣言が入っていない CI の成功(通常の開発)はここで止まる + prepare: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + # ジョブ単位の permissions はワークフロー既定を置き換える。タグの push に contents: write が要る + permissions: + contents: write + outputs: + tag: ${{ steps.derive.outputs.tag }} + version: ${{ steps.derive.outputs.version }} + centralPom: ${{ steps.derive.outputs.centralPom }} + portalMarker: ${{ steps.derive.outputs.portalMarker }} + steps: + # workflow_run の GITHUB_SHA は既定ブランチの先端であり、CI が見たコミットとは限らない。 + # 公開するのは CI が通したコミットそのもののため、ref にはその head を指定する + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + # 開発版のままなら公開する版は無い。CI の成功毎に通る経路のため、Gradle を起こす前にここで抜ける + - name: Detect a declared release version + id: declared + run: | + set -euo pipefail + current=$(sed -n 's/^enumizerVersion=//p' gradle.properties) + case "$current" in + *-SNAPSHOT) + echo "::notice::$current is a development version" + exit 0 + ;; + esac + echo "release=true" >> "$GITHUB_OUTPUT" + + - if: steps.declared.outputs.release == 'true' + uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + with: + distribution: temurin + java-version: 17 + - if: steps.declared.outputs.release == 'true' + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + # 版・公開名・plugin id の正はビルドと gradle.properties にあり、ここでは写しを持たない。 + # タグは版宣言のコミットへ付ける。既にあって別のコミットを指しているなら、この CI の成功は + # 公開済みの版を載せたまま進んだ後続コミットのものであり、公開の起点ではない + - name: Derive the release coordinates + id: derive + if: steps.declared.outputs.release == 'true' + env: + SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + version=$(./gradlew -q properties --property version | sed -n 's/^version: //p') + group=$(./gradlew -q properties --property group | sed -n 's/^group: //p') + artifact=$(./gradlew -q :sealed-class-enumizer-compiler-plugin:properties --property name \ + | sed -n 's/^name: //p') + pluginId=$(sed -n 's/^compilerPluginId=//p' gradle.properties) + tag="v$version" + existing=$(git ls-remote --tags origin "refs/tags/$tag" | awk '{ print $1 }') + if [ -n "$existing" ] && [ "$existing" != "$SHA" ]; then + echo "::notice::$tag already points at $existing" + exit 0 + fi + if [ -z "$existing" ]; then + git tag "$tag" + git push origin "$tag" + fi + # 公開済みかの判定に使う URL。Central へは 4 モジュールを 1 つのデプロイとして上げるため、 + # 到達の確認はコンパイラプラグイン 1 つで足りる(公開したプラグインが利用者のビルドで + # Maven Central から解決するのもこの成果物) + central="https://repo1.maven.org/maven2/${group//.//}/$artifact/$version/$artifact-$version.pom" + # Plugin Portal 側は plugin marker(group = plugin id・artifactId = .gradle.plugin)を見る + marker="$pluginId.gradle.plugin" + portal="https://plugins.gradle.org/m2/${pluginId//.//}/$marker/$version/$marker-$version.pom" + { + echo "version=$version" + echo "tag=$tag" + echo "centralPom=$central" + echo "portalMarker=$portal" + } >> "$GITHUB_OUTPUT" + + # Maven Central への公開。デプロイの作成から公開の確定までを 1 タスクで行う + # (publishAndReleaseToMavenCentral。validation の通過を待ち、FAILED なら落ちる)。 + # 公開は取り消せないため、上げてよいかの判断は CI の結果と下の公開可能ターゲットの検査が受け持つ。 + # validation で落ちた場合は、Portal 上の失敗したデプロイを落としてから再実行する + # (dropMavenCentralDeployment がデプロイ ID を取って落とす)。 + # vanniktech の設定値は ORG_GRADLE_PROJECT_ 接頭辞の環境変数で渡す + # (Gradle プロパティ名は大小文字を区別するため、シークレット名とは別に対応付ける) + maven-central: + needs: prepare + # 版宣言のコミットでなければ prepare の出力は空になる。後続は needs のスキップで連鎖して止まる + if: needs.prepare.outputs.tag != '' + runs-on: macos-latest + # 全ターゲットのビルドに加え、公開確定の待ち(プラグイン既定で最大 60 分)を含む + timeout-minutes: 180 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + + # ランナーが宣言ターゲットを全て賄えることを先に確かめる。賄えないターゲットの publication は + # kotlin.native.ignoreDisabledTargets により静かに落ち、成果物が欠けたまま公開が成功してしまう。 + # 実行時の担保は publish タスクの依存としても掛かっており、ここは公開の前で落とすための前倒し + - name: Verify all declared targets are publishable + run: ./gradlew :sealed-class-enumizer-runtime-api:checkPublishableTargets + + - name: Publish to Maven Central + env: + CENTRAL_POM: ${{ needs.prepare.outputs.centralPom }} + VERSION: ${{ needs.prepare.outputs.version }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} + run: | + set -euo pipefail + # 公開済みの版へのデプロイは Central が受け付けない。再実行で二重に上げないよう先に見る + # (--retry は 5xx と接続の失敗だけを吸収し、404 = 未公開はそのまま返る) + if curl -fsI --retry 3 -o /dev/null "$CENTRAL_POM"; then + echo "::notice::$VERSION is already on Maven Central" + exit 0 + fi + ./gradlew publishAndReleaseToMavenCentral + + # Plugin Portal は取り消しができないうえ、公開したプラグインは利用者のビルドでコンパイラプラグインを + # Maven Central から解決する。公開の確定後、成果物が repo1 へ行き渡るまでには時間差があるため、 + # 実際に引けることを確かめてから公開する + plugin-portal: + needs: [prepare, maven-central] + runs-on: ubuntu-latest + # 待つのは repo1 への同期だけで、掛かるのは概ね 10〜30 分。それを超える場合は同期の異常として落とす + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + - name: Wait until the release is on Maven Central + env: + CENTRAL_POM: ${{ needs.prepare.outputs.centralPom }} + run: | + set -euo pipefail + # ジョブの打ち切りではなく、待ち過ぎを理由として落とす(同期の異常と分かる形にする) + deadline=$((SECONDS + 3000)) + until curl -fsI --retry 3 -o /dev/null "$CENTRAL_POM"; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "::error::$CENTRAL_POM is still not available" + exit 1 + fi + echo "$(date -u +%H:%M:%SZ) waiting for $CENTRAL_POM" + sleep 60 + done + + - name: Publish to Gradle Plugin Portal + env: + PORTAL_MARKER: ${{ needs.prepare.outputs.portalMarker }} + VERSION: ${{ needs.prepare.outputs.version }} + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + run: | + set -euo pipefail + if curl -fsI --retry 3 -o /dev/null "$PORTAL_MARKER"; then + echo "::notice::$VERSION is already on the Gradle Plugin Portal" + exit 0 + fi + ./gradlew :sealed-class-enumizer-gradle-plugin:publishPlugins + + # 公開の記録として GitHub Release を作る。両方の公開先へ出た後に作るため、入手できない版に + # Latest release が立つことはない。本文に変更点は持たず CHANGELOG の当該節を指す(変更点の正は + # CHANGELOG であり、写しを置くと版を跨いで食い違う)。Full Changelog 行だけは GitHub の + # 自動生成と同じものを API から取る + github-release: + needs: [prepare, plugin-portal] + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + # 作業ツリーは要らない。gh は GH_REPO で対象を取る + - name: Create the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ needs.prepare.outputs.tag }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + if gh release view "$TAG" >/dev/null 2>&1; then + echo "::notice::a release for $TAG already exists" + exit 0 + fi + # CHANGELOG の版見出し `## [<版>]` に付くアンカー。記号が落ちた形になる + anchor=$(printf '%s' "$VERSION" | tr -d .) + full=$(gh api "repos/$GH_REPO/releases/generate-notes" -f tag_name="$TAG" --jq .body \ + | grep '^\*\*Full Changelog\*\*:') + gh release create "$TAG" --verify-tag --title "$TAG" --notes "$(printf '%s\n\n%s\n' \ + "Changes in this release: [CHANGELOG](https://github.com/$GH_REPO/blob/$TAG/CHANGELOG.md#$anchor)" \ + "$full")" + + # API ドキュメントの掲載。Pages のソースは Actions のため、公開されるのはこのジョブが上げる成果物であり、 + # 保管ブランチ(gh-pages)の内容が直接配信されることはない。同ブランチは過去版の出力だけを + # versions/<版>/ の形で持ち、版切替 UI の入力として使う。deploy はサイト全体を置き換えるため、 + # 過去版の保持はこの保管場所から毎回作り直すことで成り立つ + docs: + needs: [prepare, plugin-portal] + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + # 初回リリース時は保管ブランチがまだ無い。その場合は空の履歴から始める + - name: Restore archived versions + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + url="https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" + if git ls-remote --exit-code --heads "$url" gh-pages >/dev/null 2>&1; then + git clone --branch gh-pages --depth 1 "$url" build/gh-pages + else + mkdir -p build/gh-pages + git -C build/gh-pages init -b gh-pages -q + git -C build/gh-pages remote add origin "$url" + fi + mkdir -p build/gh-pages/versions + + - run: ./gradlew dokkaGenerate -PolderDocsDir=build/gh-pages/versions + + # 保管するのは各版の出力そのものとし、生成物に含まれる過去版(older/)は落とす。 + # 落とさないと版を重ねるたびに過去版が入れ子で積み上がる + - name: Archive this version + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + dest="build/gh-pages/versions/$VERSION" + rm -rf "$dest" + mkdir -p "$dest" + cp -r build/dokka/html/. "$dest/" + rm -rf "$dest/older" + + # 保管ブランチの更新は通常のコミットで行う(force push は保護設定と整合しない) + - name: Update archive branch + working-directory: build/gh-pages + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "::notice::archive is already up to date" + else + git commit -q -m "docs: $VERSION の API ドキュメントを保管する" + git push origin gh-pages + fi + + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: build/dokka/html + - id: deploy + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + + # 公開した版は消費済みのため、開発版(次パッチの -SNAPSHOT)へ戻す PR を出す。 + # 直接 push しないのは、既定ブランチが PR 経由の更新を必須としているため。 + # 配布物を説明する文書の版表記はここでは変わらない(同期の対象はリリース版を宣言している間に限るため、 + # 公開済みの版を指したまま据え置かれる) + next-version: + needs: [prepare, plugin-portal] + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + steps: + # 公開したコミットではなく既定ブランチを起点にする(両者が一致しているとは限らない) + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + - name: Open version bump pull request + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + current=$(sed -n 's/^enumizerVersion=//p' gradle.properties) + case "$current" in + *-SNAPSHOT) + echo "::notice::$current is already a development version" + exit 0 + ;; + esac + IFS=. read -r major minor patch <<< "$current" + next="$major.$minor.$((patch + 1))-SNAPSHOT" + branch="chore/next-version-$next" + # 開いている PR があれば同じ内容を持つため作り直さない(再実行で二重に出さないため) + open=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number') + if [ -n "$open" ]; then + echo "::notice::pull request #$open is already open for $branch" + exit 0 + fi + sed -i "s/^enumizerVersion=.*/enumizerVersion=$next/" gradle.properties + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$branch" + git commit -q -am "build: 開発版を $next へ進める" + # 閉じた PR のブランチが残っていることがあるため、内容を置き換える形で押す + git push -q --force origin "$branch" + gh pr create --base main --head "$branch" \ + --title "build: 開発版を $next へ進める" \ + --body "$(printf '%s\n' \ + "$VERSION の公開に伴い、開発版を次のパッチへ進める。" \ + "" \ + "配布物を説明する文書の版表記は公開済みの版を指したまま据え置かれる。" \ + "次のリリースで -SNAPSHOT を外す際にまとめて追随する。" \ + "" \ + "CI は本 PR のランを Approve and run で起こす(bot が作った PR のランは承認待ちで止まる)。")" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce377d4..a12e7a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ -# リリース準備。版宣言(`-SNAPSHOT` の除去・版表記の同期・CHANGELOG の節起こし)の PR を出すところまでと、 -# その PR がマージされた時点でのタグ付けと公開の起動を担う。公開そのものは _publish.yml が引き取る。 -# 手順は「本ワークフローを実行 → 出た PR をマージ」までで、以降はタグ付けから公開まで自動で進む。 -# Central の公開確定(Portal の validation 確認)だけが従来どおり手作業として残る。 +# リリースの入口。版宣言(`-SNAPSHOT` の除去・版表記の同期・CHANGELOG の節起こし)の PR を出すところまでを担う。 +# 手順は「本ワークフローを実行 → 出た PR の CI を Approve and run で起こす → マージ」までで、 +# 以降はマージ後の main の CI が green になった時点で、タグ付けから Maven Central・Gradle Plugin Portal +# への公開・GitHub Release・API ドキュメント・次版の PR まで CD が自動で進める。 # アクションはコミットハッシュで固定する(末尾コメントの vX.Y.Z は参照用。更新は Dependabot が追随する) name: release (manual) @@ -12,16 +12,10 @@ on: description: "自版(例: 0.1.1)。空なら現在の enumizerVersion から -SNAPSHOT を落とした値" required: false type: string - # 版宣言の PR がマージされた時点でタグを付ける。gradle.properties は版の単一情報源であり、 - # 版宣言も次版への差し戻しも必ずこのファイルを通る - push: - branches: [main] - paths: ["gradle.properties"] permissions: contents: read -# 版宣言とタグ付けは互いに前後関係を持つため直列化する concurrency: group: ${{ github.workflow }} cancel-in-progress: false @@ -29,15 +23,13 @@ concurrency: jobs: # 版宣言の PR を出す。既定ブランチは PR 経由の更新を必須とするため、直接 push はしない declare: - if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 30 # ジョブ単位の permissions はワークフロー既定を置き換える。ブランチの push に contents: write・ - # PR の作成に pull-requests: write・CI の起動に actions: write が要る + # PR の作成に pull-requests: write が要る permissions: contents: write pull-requests: write - actions: write steps: # タグではなく既定ブランチを起点にする(リリースは既定ブランチの先端から切る) - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -96,67 +88,12 @@ jobs: gh pr create --base main --head "$BRANCH" \ --title "build: リリース版 $FULL を宣言する" \ --body "$(printf '%s\n' \ - "リリース $FULL の版宣言。マージするとタグ \`v$FULL\` が付き、公開が始まる。" \ + "リリース $FULL の版宣言。マージ後の main の CI が green になった時点でタグ \`v$FULL\` が付き、公開が始まる。" \ "" \ "残りの手順:" \ "" \ "1. CHANGELOG の \`[$FULL]\` 節を確認する(\`[Unreleased]\` の記載がそのまま入っている)" \ - "2. 本 PR をマージする" \ - "3. Central Portal の validation 通過を確認して release する")" - - # GITHUB_TOKEN が作った PR は push / pull_request を起こさず必須チェックが報告されないため、 - # ブランチを指定して CI を起こす(workflow_dispatch は GITHUB_TOKEN でも起動できる)。 - # チェックはコミットの SHA へ紐付くため、この起動で PR 側の必須チェックが満たされる - - name: Start CI for the release pull request - env: - GH_TOKEN: ${{ github.token }} - BRANCH: ${{ steps.declare.outputs.branch }} - run: gh workflow run ci.yml --ref "$BRANCH" - - # 版宣言のマージでタグを付け、公開を起こす。GITHUB_TOKEN が押したタグの push はワークフローを - # 起こさないため、公開は起動をこちらから掛ける(workflow_dispatch は GITHUB_TOKEN でも起動できる) - tag: - if: github.event_name == 'push' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - # 公開の起動に要る - actions: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - - - name: Tag the declared release version - id: tag - run: | - set -euo pipefail - current=$(sed -n 's/^enumizerVersion=//p' gradle.properties) - case "$current" in - *-SNAPSHOT) - echo "::notice::$current is a development version" - exit 0 - ;; - esac - version=$(./gradlew -q properties --property version | sed -n 's/^version: //p') - if git ls-remote --exit-code --tags origin "refs/tags/v$version" >/dev/null 2>&1; then - echo "::notice::v$version already exists" - exit 0 - fi - git tag "v$version" - git push origin "v$version" - echo "tag=v$version" >> "$GITHUB_OUTPUT" - - # _publish はタグを ref に取り、起点によらず「タグの指す位置」だけを見る。手で起こす場合も - # ref にはタグを選ぶ(既定ブランチで起こすと _publish 側の版照合で落ちる)。 - # タグを付けなかった場合は出力が空になり、このステップは飛ぶ - - name: Start publishing - if: steps.tag.outputs.tag != '' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.tag.outputs.tag }} - run: gh workflow run _publish.yml --ref "$TAG" + "2. 本 PR のランを Approve and run で起こす(bot が作った PR のランは承認待ちで止まる)" \ + "3. 本 PR をマージする" \ + "" \ + "以降は自動で進む(公開・GitHub Release・API ドキュメント・次版の PR)。")" From e862cb37138524c10ee4af68d5ea7f587ba601d1 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 12 Sep 2026 21:40:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=E7=89=88=E5=AE=A3=E8=A8=80=E3=81=AE?= =?UTF-8?q?=20PR=20=E3=82=92=E3=82=84=E3=82=8A=E7=9B=B4=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=82=82=E5=90=8C=E3=81=98=E7=8A=B6=E6=85=8B=E3=81=B8=E5=8F=8E?= =?UTF-8?q?=E6=9D=9F=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公開まで進まなかった版を宣言し直せるよう、開いている PR があれば作り直さず、 閉じた PR のブランチが残っている場合は内容を置き換える。既定ブランチが既に その版を宣言している場合は、書き換えが空になることを見て明示的に落とす。 Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a12e7a8..0de039e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,6 +73,8 @@ jobs: echo "full=$full" >> "$GITHUB_OUTPUT" echo "branch=release/v$full" >> "$GITHUB_OUTPUT" + # 公開まで進まなかった版を宣言し直せるよう、やり直しても同じ状態へ収束する形にする。 + # 既に開いている PR があればそれが宣言そのものであり、作り直さない - name: Open release pull request env: GH_TOKEN: ${{ github.token }} @@ -80,11 +82,22 @@ jobs: BRANCH: ${{ steps.declare.outputs.branch }} run: | set -euo pipefail + open=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') + if [ -n "$open" ]; then + echo "::notice::pull request #$open is already open for $BRANCH" + exit 0 + fi + # 書き換えが空 = 既定ブランチが既にこの版を宣言している(マージ済みで公開待ち) + if git diff --quiet; then + echo "::error::$FULL is already declared on main" + exit 1 + fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git switch -c "$BRANCH" git commit -q -am "build: リリース版 $FULL を宣言する" - git push -q origin "$BRANCH" + # 閉じた PR のブランチが残っていることがあるため、内容を置き換える形で押す + git push -q --force origin "$BRANCH" gh pr create --base main --head "$BRANCH" \ --title "build: リリース版 $FULL を宣言する" \ --body "$(printf '%s\n' \