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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ on:
pull_request:
branches: [main]

# Read-only token. Pinned explicitly so this workflow never inherits write / PR-approval
# rights from repository defaults (Actions is allowed to create PRs for spec-sync).
permissions:
contents: read

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # read-only job; do not leave the token in .git/config

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
Expand Down Expand Up @@ -58,6 +65,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # read-only job; do not leave the token in .git/config

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand All @@ -74,6 +83,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # read-only job; do not leave the token in .git/config

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
Expand Down
121 changes: 112 additions & 9 deletions .github/workflows/spec-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ jobs:
$r.specs | ForEach-Object { " $($_.status.PadRight(12)) $($_.spec)" } | Write-Host
"drifted=$($r.drifted.ToString().ToLower())" >> $env:GITHUB_OUTPUT
"awareness=$($r.awarenessDrift.ToString().ToLower())" >> $env:GITHUB_OUTPUT
# latestSha は後段でブランチ名・スクリプトへ展開されるため、git SHA 形式であることを確認する(多層防御)。
if ($r.latestSha -notmatch '^[0-9a-f]{40}$') { throw "unexpected upstream SHA format: '$($r.latestSha)'" }
"latestSha=$($r.latestSha)" >> $env:GITHUB_OUTPUT
"manifestRef=$($r.manifestRef)" >> $env:GITHUB_OUTPUT
"compareUrl=$($r.compareUrl)" >> $env:GITHUB_OUTPUT
Expand Down Expand Up @@ -96,9 +98,12 @@ jobs:

# --- 2) 追跡 Issue の upsert(open な spec-sync Issue があれば本文更新、無ければ作成)---
- name: Upsert tracking issue
id: issue
if: steps.decide.outputs.notify == 'true'
shell: pwsh
run: |
# ネイティブコマンド(gh)の非 0 終了でステップを失敗させる(既定では無言で継続してしまう)。
$PSNativeCommandUseErrorActionPreference = $true
$title = "Upstream spec drift: line/line-openapi @ ${{ steps.drift.outputs.latestSha }}"
$body = @()
$body += "Automated by ``.github/workflows/spec-sync.yml``. Do not edit by hand; this issue is upserted each run."
Expand All @@ -111,16 +116,26 @@ jobs:
if ($existing) {
gh issue edit $existing --repo $env:REPO --title $title --body-file "$env:RUNNER_TEMP/issue-body.md"
Write-Host "updated issue #$existing"
$number = $existing
} else {
# ラベルが既存なら失敗するのは想定内(その 1 コマンドだけ失敗を許容)。
$PSNativeCommandUseErrorActionPreference = $false
gh label create spec-sync --repo $env:REPO --color 1d76db --description "Upstream OpenAPI spec drift tracking" 2>$null
gh issue create --repo $env:REPO --title $title --label spec-sync --body-file "$env:RUNNER_TEMP/issue-body.md"
$PSNativeCommandUseErrorActionPreference = $true
$url = gh issue create --repo $env:REPO --title $title --label spec-sync --body-file "$env:RUNNER_TEMP/issue-body.md"
Write-Host "created issue $url"
$number = ($url -split '/')[-1]
}
# 後段(PR 結果コメント)が参照する。
"number=$number" >> $env:GITHUB_OUTPUT
exit 0

# 同期回復時(ドリフト無し)は、残っている追跡 Issue をクローズする。
- name: Close tracking issue when back in sync
if: steps.decide.outputs.notify == 'false'
shell: pwsh
run: |
$PSNativeCommandUseErrorActionPreference = $true
$open = gh issue list --repo $env:REPO --label spec-sync --state open --json number --jq '.[].number'
foreach ($n in $open) {
if ($n) { gh issue close $n --repo $env:REPO --comment "Upstream specs are back in sync with the committed snapshot (no drift). Closed automatically by spec-sync." }
Expand All @@ -133,6 +148,7 @@ jobs:
run: dotnet tool install --global Microsoft.OpenApi.Kiota --version 1.34.1

- name: Regenerate at pinned upstream SHA
id: regen
if: steps.decide.outputs.regenerate == 'true'
shell: pwsh
run: ./scripts/generate.ps1 -Update -Ref ${{ steps.drift.outputs.latestSha }}
Expand All @@ -151,9 +167,13 @@ jobs:

# --- 4) 下書き PR の作成/更新 ---
- name: Open or update draft PR
id: pr
if: steps.decide.outputs.regenerate == 'true'
shell: pwsh
run: |
# git/gh の非 0 終了でステップを失敗させる。既定(false)だと push 拒否等を握り潰したまま
# 後続へ進み、run が緑のまま無言で失敗する(2026-10 に上流 fa5577c9 の取り込みで顕在化)。
$PSNativeCommandUseErrorActionPreference = $true
$sha = "${{ steps.drift.outputs.latestSha }}"
$short = $sha.Substring(0,8)
$branch = "spec-sync/update-$short"
Expand All @@ -166,12 +186,31 @@ jobs:
# 再生成が触るのは openapi/(spec+manifest)と src/**/Generated/** のみ。
# 対象を限定し、CI スクラッチや不測のファイルを PR に巻き込まない。
git add openapi src
if (git diff --cached --quiet) { Write-Host "no file changes to commit"; exit 0 }
git commit -m "chore(spec-sync): update LINE OpenAPI specs to $short and regenerate"
# 専用の bot ブランチ(SHA 命名で決定的)。前回 run の残りは上書きしたいが、
# レビュアーが同ブランチに積んだ fixup を握り潰さないよう --force-with-lease を使う
# (新規ブランチ=リモート追跡 ref 無しでもモダン git は許容)。
git push --force-with-lease origin $branch

# 以下 2 つは「非 0 終了=判定結果」のプローブなので一時的に失敗扱いを外す。
$PSNativeCommandUseErrorActionPreference = $false
git diff --cached --quiet
$hasChanges = $LASTEXITCODE -ne 0
# --exit-code: 該当 ref 無し=2。それ以外の非 0(認証/ネットワーク)は異常として落とす。
git ls-remote --exit-code --heads origin $branch | Out-Null
$lsRemote = $LASTEXITCODE
$PSNativeCommandUseErrorActionPreference = $true
if ($lsRemote -notin 0, 2) { throw "git ls-remote failed (exit $lsRemote)" }
$branchExists = $lsRemote -eq 0

if (-not $hasChanges) { Write-Host "no file changes to commit"; exit 0 }

# bot ブランチは上流 SHA 命名で、同じ SHA なら再生成内容は決定的。既存ブランチには push しない
# (checkout は追跡 ref を持たないため --force-with-lease は必ず stale 拒否になる。上書きすると
# レビュアーが積んだ fixup を消す)。main 側の生成器変更は既存ブランチに反映されない点を本文に明記する。
$pushNote = $null
if ($branchExists) {
$pushNote = "Branch ``$branch`` already existed, so this run did **not** re-push it (reviewer fixups are preserved). If ``scripts/generate.ps1`` or the Kiota version changed on ``main`` since, regenerate on the branch manually. The build/test results below are from this run's regeneration on ``main``, not from the branch contents."
Write-Host "::notice::branch $branch already exists on origin; skipping push"
} else {
git commit -m "chore(spec-sync): update LINE OpenAPI specs to $short and regenerate"
git push origin $branch
}

# PR 本文(チェックリスト付き)
$pb = @()
Expand All @@ -180,23 +219,87 @@ jobs:
$pb += (Get-Content "$env:RUNNER_TEMP/drift-summary.md" -Raw)
$pb += ""
$pb += "### CI (this run)"
$pb += "- run: $env:GITHUB_SERVER_URL/$env:REPO/actions/runs/$env:GITHUB_RUN_ID"
$pb += "- build: ``$buildOutcome``"
$pb += "- test: ``$testOutcome`` (public API snapshot test catches breaking changes to the hand-written surface)"
$pb += "- PRs opened with ``GITHUB_TOKEN`` do not trigger ``ci.yml``; close/reopen the PR (or push a commit) to run the full CI before merging."
if ($pushNote) { $pb += ""; $pb += "> $pushNote" }
$pb += ""
$pb += "### Reviewer checklist"
$pb += "- [ ] ``src/**/Generated/**`` の差分に新規オペレーション/モデルが無いか(あれば手書きファサード要検討=公開 API snapshot には出ない)"
$pb += "- [ ] 公開 API snapshot テスト結果(破壊的変更の有無)を確認"
$pb += "- [ ] 4役ゲート(code/security/spec/test-arch)を実施し ``docs/reviews/`` に記録"
$pb += "- [ ] ``channel-access-token.yml`` の urn 正規化が効いているか(ビルド成功で担保)"
$pb += "- [ ] ``openapi/upstream-manifest.json`` の ref/hash が更新されているか"
$pb += "- [ ] ``openapi/upstream-manifest.json`` の ref/refDate/hash が更新されているか"
$issueNumber = "${{ steps.issue.outputs.number }}"
if ($issueNumber) { $pb += ""; $pb += "Closes #$issueNumber" }
($pb -join "`n") | Set-Content "$env:RUNNER_TEMP/pr-body.md"

$existingPr = gh pr list --repo $env:REPO --head $branch --state open --json number --jq '.[0].number'
# 人が却下(未マージで close)した PR は作り直さない(上流 SHA が変わらない限り毎週再作成されるのを防ぐ)。
$closedPr = gh pr list --repo $env:REPO --head $branch --state closed --json number,mergedAt --jq '[.[] | select(.mergedAt == null)][0].number'
if (-not $existingPr -and $closedPr) {
Write-Host "::notice::PR #$closedPr for $branch was closed without merging; not reopening"
"closedPr=$closedPr" >> $env:GITHUB_OUTPUT
exit 0
}
if ($existingPr) {
gh pr edit $existingPr --repo $env:REPO --body-file "$env:RUNNER_TEMP/pr-body.md"
Write-Host "updated PR #$existingPr"
$prUrl = gh pr view $existingPr --repo $env:REPO --json url --jq .url
} else {
gh pr create --repo $env:REPO --draft --base main --head $branch `
$prUrl = gh pr create --repo $env:REPO --draft --base main --head $branch `
--title "chore(spec-sync): update specs to $short and regenerate" `
--label spec-sync --body-file "$env:RUNNER_TEMP/pr-body.md"
Write-Host "created PR $prUrl"
}
"url=$prUrl" >> $env:GITHUB_OUTPUT
"pushed=$((-not $branchExists).ToString().ToLower())" >> $env:GITHUB_OUTPUT
exit 0

# --- 5) 結果を追跡 Issue へ記録(PR 作成失敗時も残す)---
- name: Report to tracking issue
if: ${{ !cancelled() && steps.decide.outputs.regenerate == 'true' && steps.issue.outputs.number != '' }}
shell: pwsh
# ステップ出力は式でスクリプト本文へ直接展開せず env 経由で渡す(式インジェクション対策の定石)。
env:
ISSUE_NUMBER: ${{ steps.issue.outputs.number }}
REGEN_OUTCOME: ${{ steps.regen.outcome }}
BUILD_OUTCOME: ${{ steps.build.outcome }}
TEST_OUTCOME: ${{ steps.test.outcome }}
PR_OUTCOME: ${{ steps.pr.outcome }}
PR_URL: ${{ steps.pr.outputs.url }}
PR_PUSHED: ${{ steps.pr.outputs.pushed }}
CLOSED_PR: ${{ steps.pr.outputs.closedPr }}
run: |
$PSNativeCommandUseErrorActionPreference = $true
$c = @()
$c += "spec-sync run: $env:GITHUB_SERVER_URL/$env:REPO/actions/runs/$env:GITHUB_RUN_ID"
$c += ""
$c += "- regenerate: ``$env:REGEN_OUTCOME``"
$c += "- build: ``$env:BUILD_OUTCOME``"
$c += "- test: ``$env:TEST_OUTCOME``"
$c += "- draft PR step: ``$env:PR_OUTCOME``"
if ($env:PR_URL) {
$c += "- PR: $env:PR_URL"
if ($env:PR_PUSHED -eq "false") { $c += "- branch already existed; not re-pushed" }
} elseif ($env:CLOSED_PR) {
$c += "- PR #$env:CLOSED_PR for this upstream SHA was closed without merging, so no new PR was opened. Reopen it (or delete the branch) to retry."
} elseif ($env:REGEN_OUTCOME -ne "success") {
$c += "- **Regeneration failed**, so no PR was opened. See the run log."
} else {
$c += "- **No PR was opened/updated.** Check the run log (no file changes after regeneration, the repository setting *Allow GitHub Actions to create and approve pull requests*, or a git/gh failure)."
}
($c -join "`n") | Set-Content "$env:RUNNER_TEMP/issue-comment.md"
gh issue comment $env:ISSUE_NUMBER --repo $env:REPO --body-file "$env:RUNNER_TEMP/issue-comment.md"

# Build/Test は PR 本文へ結果を載せるため continue-on-error だが、失敗を run の結果(通知)にも出す。
- name: Fail run when regenerated client does not build or pass tests
if: steps.decide.outputs.regenerate == 'true' && (steps.build.outcome != 'success' || steps.test.outcome != 'success')
shell: pwsh
env:
BUILD_OUTCOME: ${{ steps.build.outcome }}
TEST_OUTCOME: ${{ steps.test.outcome }}
run: |
Write-Host "::error::regenerated client: build=$env:BUILD_OUTCOME test=$env:TEST_OUTCOME (see the draft PR / tracking issue)"
exit 1
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ dotnet docfx docs/manual/docfx.json # metadata + build → docs/manual/_s
- **検知**: `pwsh scripts/check-spec-drift.ps1`(manifest 基準・純検知・ドリフト時 exit 1・`-Json`/`-FailOnAwareness` あり)。gh 優先・無ければ REST。
- **再取得+再生成**: `pwsh scripts/generate.ps1 -Update [-Ref <sha>]`(SHA ピン再取得→正規化→manifest 更新→Kiota 生成)。既定(`-Update` 無し)は同梱 spec を使う再現生成で挙動不変。
- **週次自動化**: `.github/workflows/spec-sync.yml`(cron+手動)が 検知→`spec-sync` ラベルの Issue upsert(回復時 close)→再生成→**draft PR** 自動作成。**マージは常に人+4 役ゲート**(自動マージしない)。破壊的変更は公開 API snapshot が捕捉、生成コードのみの追加は PR チェックリストで人手確認。
- **前提:** リポジトリ設定 *Allow GitHub Actions to create and approve pull requests* がオン(オフだと PR 作成だけ失敗する)。既存の bot ブランチ(`spec-sync/update-<sha8>`)には push しない(同一 SHA は決定的・fixup 保護)。git/gh の失敗と再生成の build/test 失敗は run を失敗させ、結果は追跡 Issue にコメントされる。`GITHUB_TOKEN` 製の PR では `ci.yml` が走らないので、マージ前に close/reopen でフル CI を回す。
- **⚠️ pwsh 7 の `ConvertFrom-Json`/`Invoke-RestMethod` は ISO 日付を `DateTime` に自動変換する**(文字列前提の処理は例外になる。`generate.ps1` の `refDate` で踏んだ)。

## 規約

Expand Down
6 changes: 6 additions & 0 deletions docs/LINE-dotnet-client-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ Webhook(モデルのみ)/LIFF も同様に個別生成。
2. **追跡 Issue upsert** — drift 時、`spec-sync` ラベルの Issue を作成/更新(compare リンク・spec 別状態・上流コミット一覧)。同期回復時は自動クローズ。
3. **再生成** `scripts/generate.ps1 -Update -Ref <latestSha>` — SHA ピンで再取得 → 正規化 → manifest 更新 → Kiota 再生成 → build + test。**Kiota CLI 版は 1.34.1 にピン**(R3・生成物との整合)。
4. **下書き PR** — 再生成コード+正規化 spec+manifest を含む draft PR を自動作成。**マージは常に人+4役ゲート**(自動マージしない)。
- 前提: リポジトリ設定 *Allow GitHub Actions to create and approve pull requests* がオン(オフだと PR 作成が失敗する)。
- bot ブランチ `spec-sync/update-<sha8>` が既にリモートにあれば **push しない**(同一 SHA の再生成は決定的・レビュアーの fixup を保護。checkout は追跡 ref を持たないため lease 付き上書きは成立しない)。PR の作成/本文更新のみ行い、スキップした旨を本文に明記する。
- 同じブランチの PR が**未マージで close**(人が却下)されていれば作り直さない(同一上流 SHA の間、毎週再作成されるのを防ぐ)。再試行は PR を reopen するかブランチを削除する。
- git/gh の非 0 終了はステップを失敗させる(`$PSNativeCommandUseErrorActionPreference`)。無言失敗させない。
5. **結果の記録** — run URL・build/test 結果・PR リンク(または PR 未作成の旨)を追跡 Issue にコメント(PR 作成失敗時も残す)。再生成クライアントの build/test 失敗は最終ステップで run 自体を失敗扱いにする(通知が届く)。
- `GITHUB_TOKEN` で作成した PR では `ci.yml` が起動しない(GitHub 仕様)。マージ前に close/reopen 等でフル CI を走らせる。

破壊的変更は公開 API snapshot テストが捕捉。**生成コードだけの追加(新オペレーション/モデル)は snapshot に出ない**ため、PR 本文のレビュアーチェックリストで人手確認する(手書きファサードの要否判断)。全パッケージは `Line.Core` + Kiota ランタイム版にロックステップ連動。

Expand Down
Loading
Loading