Skip to content
Draft
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
10 changes: 7 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 依存の version updates(マニフェスト = version catalog / ビルドスクリプト / ワークフロー由来)。
# Kotlin の更新 PR は catalog 経由で配布版 <KotlinVersion>-<自版> の導出まで自動で追随し、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 が
Expand All @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/version-sync.yml
Original file line number Diff line number Diff line change
@@ -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"
100 changes: 91 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 の <maven.version> → 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()
Expand All @@ -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 が持つ <maven.version> から導出する
// (syncMavenVersion が書き戻し、checkMavenVersion が乖離を検出する)。
// 親 POM は座標を決め打ちで解決したうえで、子 POM の <parent> と照合し、
// 上流のレイアウト変更を黙って取り込まないようにする

// POM 自体はビルドの依存ではなく参照する入力に過ぎないため、detached configuration で取得する
// (名前付きの構成として公開すると依存グラフの登録対象にも入ってしまう)
fun pomOf(notation: String): FileCollection =
configurations
.detachedConfiguration(dependencies.create(notation))
.apply { isTransitive = false }
.incoming
.files

// POM の解決を伴うため、値の取り出しは構成時ではなく実行時に行う
val upstreamMavenVersion: Provider<String> = 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("""<parent>(.*?)</parent>""", RegexOption.DOT_MATCHES_ALL).find(child)
) {
"kotlin-maven-plugin の POM に <parent> がありません"
}
.groupValues[1]
val parentArtifact =
checkNotNull(Regex("""<artifactId>([^<]+)</artifactId>""").find(parentBlock)) {
"kotlin-maven-plugin の POM の <parent> に artifactId がありません"
}
.groupValues[1]
check(parentArtifact == "kotlin-project") {
"kotlin-maven-plugin の親 POM が $parentArtifact へ変わっています。導出元の座標を見直してください"
}
checkNotNull(
Regex("""<maven\.version>([^<]+)</maven\.version>""")
.find(parentPom.singleFile.readText())
) {
"親 POM に <maven.version> がありません"
}
.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<Pair<Regex, String>> =
listOf(
Expand Down Expand Up @@ -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、末尾カンマ付与)で揃える
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
kotlin = "2.4.10"
# maven-plugin がコンパイル時に参照する Maven API の版(kotlin-maven-plugin 自身が依存する版に揃える)
# maven-plugin がコンパイル時に参照する Maven API の版。kotlin-maven-plugin の親 POM の
# <maven.version> から導出される値であり、手で選ばない(ルートの syncMavenVersion が書き戻す)
maven = "3.9.12"
autoservice = "0.1.5"
ktfmt = "0.26.0"
Expand Down