diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 067f3ec..b61e4e1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,8 @@ # 依存の version updates(マニフェスト = version catalog / ビルドスクリプト / ワークフロー由来)。 # Kotlin の更新 PR は catalog 経由で配布版 -<自版> の導出まで自動で追随し、CI が検証する。 -# README / docs / .idea 中の現行バージョン表記はマニフェスト更新では変わらないため、 -# CI(ルートの checkVersionMentions)が乖離を検出したら、./gradlew syncVersionMentions の -# 実行結果を更新 PR へコミットして追随させる。 +# catalog の maven 版・README / docs / .idea 中の現行バージョン表記はマニフェスト更新では変わらないため、 +# CI(ルートの checkMavenVersion / checkVersionMentions)が乖離を検出し、 +# version-sync ワークフローが同期結果を更新 PR へコミットする。 # 解決済み依存グラフ(推移依存含む)に対する alerts は build.yml の dependency-submission ジョブが補完する # # npm(kotlin-js-store/yarn.lock)は version updates の対象に含めない。lockfile は KGP が @@ -23,6 +23,10 @@ updates: day: "friday" time: "19:00" timezone: "Asia/Tokyo" + ignore: + # maven 版は kotlin-maven-plugin の親 POM から導出する値であり(ルートの syncMavenVersion)、 + # 独立して選べる更新対象ではない。Kotlin の更新に伴って追随する + - dependency-name: "org.apache.maven:maven-core" # integration-test は独立した Gradle ビルド(バージョン宣言は親カタログ共有のため、対象は settings 等の残余) - package-ecosystem: "gradle" directory: "/integration-test" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fddc63..1671b06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,11 @@ name: build # PR では起動条件に paths を置かない。paths でスキップされたワークフローのチェックは pending のまま残り、 # 必須チェック(ci-gate)が報告されずマージがブロックされるため(docs/チェックリスト.md §8 R-6)。 # 判定は changes ジョブへ降ろし、各ジョブの実行条件として扱う +# +# workflow_dispatch は version-sync が同期コミットに対する再検証を起こすための入口 +# (GITHUB_TOKEN による push は再帰防止で run を作らない)。手動起動の用途は持たない on: + workflow_dispatch: push: branches: [main] paths: @@ -219,10 +223,10 @@ jobs: key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} - run: ./gradlew.bat -p integration-test :mpp-producer:mingwX64Test - # 現行バージョン表記と単一情報源の一致検証(ルートの checkVersionMentions。乖離時は - # ./gradlew syncVersionMentions の結果をコミットして追随する)。ビルド入力を変更する PR + # 現行バージョン表記と単一情報源の一致検証(ルートの checkVersionMentions)。ビルド入力を変更する PR # (Dependabot の版更新を含む)は build ジョブの check が同じ検証を含むため、このジョブは - # ビルド入力の allowlist から除外されている .md など、表記側だけを変更する PR を拾う + # ビルド入力の allowlist から除外されている .md など、表記側だけを変更する PR を拾う。 + # 乖離での失敗は version-sync ワークフローが受けて、同期結果を PR ブランチへコミットする version-mentions: needs: changes if: needs.changes.outputs.versionMentions == 'true' diff --git a/.github/workflows/version-sync.yml b/.github/workflows/version-sync.yml new file mode 100644 index 0000000..214986e --- /dev/null +++ b/.github/workflows/version-sync.yml @@ -0,0 +1,77 @@ +# 単一情報源から導出される版の生成物(version catalog の maven 版・README / docs / .idea の現行版表記)を +# PR ブランチへ自動追随させる。Dependabot の版更新はマニフェストしか書き換えないため、 +# build ワークフローの checkMavenVersion / checkVersionMentions が乖離を検出して失敗する。 +# その失敗を受けて本ワークフローが同期タスクを実行し、結果を PR ブランチへコミットする。 +# +# workflow_run を使う理由: Dependabot 起因の run は read-only な GITHUB_TOKEN で走り、 +# 通常の secrets も参照できないため、PR ブランチへ push できない。workflow_run はデフォルトブランチの +# 定義が書き込み可能なトークンで起動されるため、この制約を受けない。 +# 併せて、write 権限を持った状態で PR head のコードを実行する範囲を絞る(同一リポジトリのブランチに限定し、 +# fork からの PR は対象外とする)。 +# +# push 後に build を明示起動する理由: GITHUB_TOKEN による push は再帰防止で新しい run を作らないため、 +# 同期コミットに対する検証が動かず PR のチェックが待ちのまま止まる。 +# workflow_dispatch はトークン種別によらず run を作る例外にあたるため、これで起動する +name: version-sync + +on: + workflow_run: + workflows: [build] + types: [completed] + +permissions: + contents: read + +# 同一ブランチへの同期を直列化する。既存の同期を打ち切ると中途半端なコミットが残りうるため中断はしない +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + sync: + # 成功した run には同期すべき乖離が無い(両 check は build ワークフローの check に紐付く)。 + # workflow_dispatch 起因の run(下の再検証)は対象外となり、同期は繰り返されない + if: >- + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'failure' && + github.event.workflow_run.head_repository.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 30 + # ジョブ単位の permissions はワークフロー既定を置き換える。 + # コミットの push に contents: write・build の起動に actions: write が要る + permissions: + contents: write + actions: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_branch }} + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + # 上流 POM → catalog → 資料 の順に伝播させる。catalog の値は Gradle の構成時に読まれるため、 + # 前段の書き戻しを後段へ反映するには起動を分ける必要がある(ルートの build.gradle.kts) + - run: ./gradlew syncMavenVersion + - run: ./gradlew syncVersionMentions + - id: commit + env: + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: | + # 同期タスクの出力先だけを対象にする(-u で追跡済みファイルに限る) + git add -u -- gradle/libs.versions.toml README.md docs .idea/kotlinc.xml + if git diff --cached --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "build: 単一情報源から導出される版表記を追随させる" + git push origin "HEAD:$HEAD_BRANCH" + echo "changed=true" >> "$GITHUB_OUTPUT" + - if: steps.commit.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: gh workflow run build.yml --ref "$HEAD_BRANCH" diff --git a/build.gradle.kts b/build.gradle.kts index 7363f02..1a7d9ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,14 +32,15 @@ allprojects { version = enumizerFullVersion } -// README / docs / .idea 中の「現行バージョン」表記を単一情報源(Kotlin / Maven = version catalog・ -// 自版 = enumizerVersion・Gradle = wrapper properties)から導出した値へ揃える。 -// Dependabot 等の版更新はマニフェストしか書き換えず表記が置き去りになるため、 -// checkVersionMentions(check へ紐付け。CI では version-mentions ジョブ)が乖離を検出し、 -// syncVersionMentions が書き換える。 -// 置換は「現行版を指す表記」に限る。サポート下限("Kotlin 2.4" / "Gradle 9+" / "Maven 3.9+")・ -// 特定版に固定した実測エビデンス(設計00 の "Kotlin 2.4.0 実測")・別版での版形式の例示は -// 書き換えてはならないため、対象ファイルの許可リストと文脈付きパターンの両方で絞る +// 版は次の 2 段で単一情報源から導出する。上流ほど源に近く、下流は上流の値を写した生成物として扱う。 +// 1. kotlin-maven-plugin の親 POM の → version catalog の maven 版(syncMavenVersion) +// 2. version catalog / enumizerVersion / wrapper properties → README / docs / .idea 中の現行版表記 +// (syncVersionMentions) +// カタログの値は Gradle の構成時に読まれるため、1 の書き戻しを 2 へ反映するには起動を分ける必要がある +// (CI の version-sync ワークフローは 2 ステップに分けて実行する)。 +// Dependabot 等の版更新はマニフェストしか書き換えず下流が置き去りになるため、 +// check へ紐付けた checkMavenVersion / checkVersionMentions が乖離を検出し、 +// version-sync ワークフローが同期結果を PR ブランチへコミットする fun minorOf(version: String) = version.split(".").take(2).joinToString(".") val kotlinCurrent = libs.versions.kotlin.get() @@ -53,6 +54,87 @@ val gradleWrapperVersion = .map { checkNotNull(Regex("""/gradle-(\d+(?:\.\d+)+)-""").find(it)).groupValues[1] } .get() +// maven-core は kotlin-maven-plugin の realm 側が実行時に供給するため compileOnly で参照する。 +// その版は kotlin-maven-plugin 自身が provided スコープで宣言する版が正だが、provided は Gradle の +// POM 取り込み対象外で推移解決されないため、カタログのリテラルとして持つ必要がある。 +// リテラルは手で選ばず、kotlin-maven-plugin の親 POM が持つ から導出する +// (syncMavenVersion が書き戻し、checkMavenVersion が乖離を検出する)。 +// 親 POM は座標を決め打ちで解決したうえで、子 POM の と照合し、 +// 上流のレイアウト変更を黙って取り込まないようにする + +// POM 自体はビルドの依存ではなく参照する入力に過ぎないため、detached configuration で取得する +// (名前付きの構成として公開すると依存グラフの登録対象にも入ってしまう) +fun pomOf(notation: String): FileCollection = + configurations + .detachedConfiguration(dependencies.create(notation)) + .apply { isTransitive = false } + .incoming + .files + +// POM の解決を伴うため、値の取り出しは構成時ではなく実行時に行う +val upstreamMavenVersion: Provider = run { + val childPom = pomOf("org.jetbrains.kotlin:kotlin-maven-plugin:$kotlinCurrent@pom") + val parentPom = pomOf("org.jetbrains.kotlin:kotlin-project:$kotlinCurrent@pom") + providers.provider { + val child = childPom.singleFile.readText() + val parentBlock = + checkNotNull( + Regex("""(.*?)""", RegexOption.DOT_MATCHES_ALL).find(child) + ) { + "kotlin-maven-plugin の POM に がありません" + } + .groupValues[1] + val parentArtifact = + checkNotNull(Regex("""([^<]+)""").find(parentBlock)) { + "kotlin-maven-plugin の POM の に artifactId がありません" + } + .groupValues[1] + check(parentArtifact == "kotlin-project") { + "kotlin-maven-plugin の親 POM が $parentArtifact へ変わっています。導出元の座標を見直してください" + } + checkNotNull( + Regex("""([^<]+)""") + .find(parentPom.singleFile.readText()) + ) { + "親 POM に がありません" + } + .groupValues[1] + } +} + +val versionCatalogFile = layout.projectDirectory.file("gradle/libs.versions.toml").asFile + +val mavenVersionEntry = Regex("""^maven = "[^"]*"$""", RegexOption.MULTILINE) + +tasks.register("syncMavenVersion") { + val upstream = upstreamMavenVersion + val file = versionCatalogFile + val entry = mavenVersionEntry + doLast { + val current = file.readText() + val synced = entry.replace(current, "maven = \"${upstream.get()}\"") + if (synced != current) file.writeText(synced) + } +} + +val checkMavenVersion = + tasks.register("checkMavenVersion") { + val upstream = upstreamMavenVersion + val declared = mavenCurrent + doLast { + val expected = upstream.get() + if (declared != expected) { + throw GradleException( + "maven 版が kotlin-maven-plugin の参照版とズレています(宣言 $declared / 上流 $expected)。" + + "./gradlew syncMavenVersion で追随してください" + ) + } + } + } + +// 置換は「現行版を指す表記」に限る。サポート下限("Kotlin 2.4" / "Gradle 9+" / "Maven 3.9+")・ +// 特定版に固定した実測エビデンス(設計00 の "Kotlin 2.4.0 実測")・別版での版形式の例示は +// 書き換えてはならないため、対象ファイルの許可リストと文脈付きパターンの両方で絞る。 // 3 成分(パッチ付き)と 2 成分(マイナーまで)の表記は、それぞれの粒度を保ったまま現行値へ置換する val versionMentionRules: List> = listOf( @@ -120,7 +202,7 @@ val checkVersionMentions = } } -tasks.named("check") { dependsOn(checkVersionMentions) } +tasks.named("check") { dependsOn(checkVersionMentions, checkMavenVersion) } // gradle.properties の kotlin.code.style=official に合わせ、ktfmt も Kotlin 公式スタイル // (ブロック・継続ともインデント 4、末尾カンマ付与)で揃える diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4ec642a..6e3d935 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] kotlin = "2.4.10" -# maven-plugin がコンパイル時に参照する Maven API の版(kotlin-maven-plugin 自身が依存する版に揃える) +# maven-plugin がコンパイル時に参照する Maven API の版。kotlin-maven-plugin の親 POM の +# から導出される値であり、手で選ばない(ルートの syncMavenVersion が書き戻す) maven = "3.9.12" autoservice = "0.1.5" ktfmt = "0.26.0"