diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index caaae02c..85cd2755 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -96,8 +96,8 @@ jobs: echo "---" echo "" echo "このPRへのpushの度に、Xcode Cloudの「Upload Release Candidate TestFlight」workflowが" - echo "外部TestFlightにビルドを配信します。マージすると、タグ作成・GitHub Release publish・" - echo "メタデータ同期・Xcode CloudのUpload For AppStore起動が自動で走ります(doc/adr/0016)。" + echo "外部TestFlightにビルドを配信し、「Upload For AppStore」workflowがApp Store Connectへビルドを" + echo "アップロードします。マージするとタグ作成・GitHub Release publishだけが走ります(doc/adr/0027)。" } > pr_body.md - name: Create pull request diff --git a/.github/workflows/release-merged.yml b/.github/workflows/release-merged.yml index f9d18e53..b10a9580 100644 --- a/.github/workflows/release-merged.yml +++ b/.github/workflows/release-merged.yml @@ -1,15 +1,14 @@ name: Release Merged -# doc/adr/0016: リリースPR(release/*ブランチ)がmainにマージされたことをトリガーに、 -# タグ作成 → GitHub Release publish → メタデータ同期 → Xcode Cloud起動 の順で実行する。 -# 実行順序を保証するため、あえて1ジョブの直列ステップとして書く(並列ジョブにしない)。 +# doc/adr/0027: リリースPR(release/*ブランチ)がmainにマージされたことをトリガーに、 +# タグ作成 → GitHub Release publish だけを行う。 +# メタデータ同期(sync-metadata.yml)とApp Storeへのビルドアップロードはここからは起動しない。 on: pull_request: types: [closed] permissions: contents: write - actions: write jobs: release: @@ -46,31 +45,17 @@ jobs: - name: Publish GitHub Release run: | + # 同じバージョンのReleaseが既にあれば削除して作り直す(タグと同様に上書き扱いにする)。 + # --cleanup-tagは付けない(直前でpushしたタグまで消えてしまうため) + if gh release view "$VERSION" > /dev/null 2>&1; then + gh release delete "$VERSION" --yes + fi gh release create "$VERSION" \ --title "$VERSION" \ --generate-notes \ --latest \ --target main - - name: Trigger metadata sync workflow - run: gh workflow run sync-metadata.yml --ref main -f version="$VERSION" - - - name: Trigger Xcode Cloud Upload For AppStore build - env: - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} - ASC_SECRET_KEY: ${{ secrets.ASC_SECRET_KEY }} - XCODE_CLOUD_APPSTORE_WORKFLOW_ID: ${{ secrets.XCODE_CLOUD_APPSTORE_WORKFLOW_ID }} - run: | - KEY_PATH="$(mktemp)" - echo "$ASC_SECRET_KEY" | base64 -d > "$KEY_PATH" - if [ ! -s "$KEY_PATH" ]; then - echo "::error::ASC_SECRET_KEY のデコード結果が空でした。シークレットの設定値を確認してください" - exit 1 - fi - ASC_API_KEY_PATH="$KEY_PATH" ./scripts/trigger-xcode-cloud-build.sh "$XCODE_CLOUD_APPSTORE_WORKFLOW_ID" main - rm -f "$KEY_PATH" - - name: Create summary if: always() run: | diff --git a/.github/workflows/sync-metadata.yml b/.github/workflows/sync-metadata.yml index 528179bd..ec1d8640 100644 --- a/.github/workflows/sync-metadata.yml +++ b/.github/workflows/sync-metadata.yml @@ -1,11 +1,10 @@ name: Sync Metadata -# doc/adr/0016: release-merged.yml からのGitHub Release publish後にキックされる。 -# fastlane/metadata配下のテキストメタデータをApp Store Connectへ同期する +# doc/adr/0016: fastlane/metadata配下のテキストメタデータをApp Store Connectへ同期する # (fastlane sync_metadataレーン、Fastfile参照)。ビルドのアップロードは行わない。 # versionはASC上のバージョン作成にも使うため必須。対象バージョンがASCに無ければ新規作成し、 # 既にあればメタデータの更新のみ行う(Fastfileのsync_metadataレーン参照)。 -# 単独での再実行やスクリーンショット追加後の確認用に workflow_dispatch でも起動できる。 +# release-merged.yml からは起動しない(doc/adr/0027)。必要なときに workflow_dispatch で手動実行する。 on: workflow_dispatch: inputs: diff --git a/ci_scripts/ci_post_xcodebuild.sh b/ci_scripts/ci_post_xcodebuild.sh index 0727eef5..141cb235 100755 --- a/ci_scripts/ci_post_xcodebuild.sh +++ b/ci_scripts/ci_post_xcodebuild.sh @@ -63,8 +63,7 @@ case "$CI_WORKFLOW" in echo "=== Upload For AppStore workflow ===" # doc/adr/0016: バージョンタグの作成はGitHub Actions側(release-merged.yml、 - # リリースPRのmainマージ時)に一本化した。このworkflowはGitHub Actions側から - # APIでキックされる時点で既にタグが打たれている前提のため、ここでは何もしない。 + # リリースPRのmainマージ時)に一本化したため、ここでは何もしない。 echo "Tag creation is handled by GitHub Actions (release-merged.yml). Nothing to do here." echo "===========================" ;; diff --git a/doc/adr/0016-release-pipeline-automation.md b/doc/adr/0016-release-pipeline-automation.md index a80dd245..2bd84cfe 100644 --- a/doc/adr/0016-release-pipeline-automation.md +++ b/doc/adr/0016-release-pipeline-automation.md @@ -1,6 +1,6 @@ ## タイトル: リリースPR作成〜App Store提出までのパイプラインを自動化し、タグ作成をGitHub Actions側に一本化する -* **ステータス: 承認済** +* **ステータス: 承認済**(リリースPRマージ時のメタデータ同期・Xcode Cloud起動は [ADR-0027](0027-release-merged-tag-and-release-only.md) で廃止) * 意思決定者: @stotic-dev * 日付: 2026-09-05 * 技術的背景やその他関連チケット No: [#237](https://github.com/stotic-dev/homete_iOS/issues/237) / [#238](https://github.com/stotic-dev/homete_iOS/issues/238) diff --git a/doc/adr/0027-release-merged-tag-and-release-only.md b/doc/adr/0027-release-merged-tag-and-release-only.md new file mode 100644 index 00000000..d3a2d46d --- /dev/null +++ b/doc/adr/0027-release-merged-tag-and-release-only.md @@ -0,0 +1,46 @@ +## タイトル: リリースPRマージ時の自動処理をタグ作成とGitHub Release公開だけに絞る + +* **ステータス: 承認済** +* 意思決定者: @stotic-dev +* 日付: 2026-09-29 +* 技術的背景やその他関連チケット No: [ADR-0016](0016-release-pipeline-automation.md) の一部を置き換える + +## 文脈、背景や問題点の説明 + +[ADR-0016](0016-release-pipeline-automation.md) では、リリースPRのmainマージをトリガーに `release-merged.yml` が「タグ作成 → GitHub Release publish → メタデータ同期 → Xcode Cloud起動」を直列で実行する構成にした。 + +v1.0.0のリリースで、このうちXcode Cloud起動のステップが失敗した(`XCODE_CLOUD_APPSTORE_WORKFLOW_ID` のシークレットが登録されておらず、`scripts/trigger-xcode-cloud-build.sh` に空のワークフローIDが渡った)。一方で、Xcode Cloudの「Upload For AppStore」は開始条件が「すべてのタグ」になっており、`release-merged.yml` がpushしたタグで起動していた。つまりAPIでの起動ステップは不要で、マージ時にメタデータ同期とApp Storeへのビルドアップロードが自動で走ること自体も望んでいなかった。マージ時に自動で走らせたい処理は、タグ付けとGitHub Releaseの公開だけだった。 + +## 決定事項 + +* `release-merged.yml` が行う処理を「タグ作成・push」と「GitHub Release作成・publish」だけにする +* メタデータ同期(`sync-metadata.yml`)の起動と、Xcode Cloud「Upload For AppStore」の起動をワークフローから外す + * `sync-metadata.yml` 自体は残し、必要なときに `workflow_dispatch` で手動実行する +* 呼び出し元がなくなった `scripts/trigger-xcode-cloud-build.sh` を削除する +* 起動するワークフローがなくなったので、`release-merged.yml` の `actions: write` 権限を外す +* Xcode Cloud「Upload For AppStore」の開始条件を「すべてのタグ」から「`release/` ブランチへの変更」に変える(App Store Connect側の設定のため、リポジトリでは管理しない) + * タグのpushでは起動しなくなり、リリースPRへのpushの度にApp Store Connectへビルドがアップロードされる + +## 考慮した選択肢 + +* **シークレットを登録してADR-0016の構成を維持する**: マージからビルドのアップロードまで自動化できる。ただし、`ciBuildRuns` を作成する部分は一度も動作確認できておらず、自動化したい範囲でもないため採用しない +* **タグ作成とGitHub Release公開だけに絞る**: 採用 + +## 決定結果 + +### 決定にあたり考慮したメリット + +* 動作確認できていないAPI呼び出しや、未登録のシークレットが原因でリリースワークフローが失敗しなくなる +* リリースPRのマージがApp Store Connectへの反映を伴わなくなり、マージの影響範囲がタグとGitHub Releaseに限られる +* ワークフローが必要とするシークレット・権限が減る(ASC APIキー、`actions: write` が不要になる) + +### 決定にあたり考慮したデメリット + +* メタデータ同期は必要なときに手動で実行する必要がある +* 「Upload For AppStore」の開始条件はApp Store Connect側の設定で、リポジトリの記述と食い違っても気付きにくい + +## 参考 + +* [ADR-0016](0016-release-pipeline-automation.md) +* `.github/workflows/release-merged.yml` +* `.github/workflows/sync-metadata.yml` diff --git a/fastlane/Fastfile b/fastlane/Fastfile index cbc447be..5a867100 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -56,8 +56,7 @@ platform :ios do ) end - # 通常のリリースはGitHub Actions(release-merged.yml)がXcode Cloudの - # 「Upload For AppStore」workflowをキックする形に移行した(doc/adr/0016)。 + # 通常のリリースはXcode Cloudの「Upload For AppStore」workflow(release/*ブランチへのpushで起動)で行う(doc/adr/0027)。 # このレーンはローカルから手動でビルド・アップロードする場合の代替手段として残す。 # タグ作成もGitHub Actions側(PRマージ時)で行うようになったため、ここでは打たない。 desc "指定されたipaでApp Storeリリースを行う(ローカル手動実行用のフォールバック)" diff --git a/scripts/trigger-xcode-cloud-build.sh b/scripts/trigger-xcode-cloud-build.sh deleted file mode 100755 index 7451205a..00000000 --- a/scripts/trigger-xcode-cloud-build.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/bin/bash -# -# App Store Connect API経由でXcode Cloudのビルドを開始するCLI。 -# JWT生成・API呼び出しの仕組みはscripts/appstoreconnect.shと同じ実装を踏襲している -# (そちらはSubscription操作で実機確認済み。このスクリプトのciBuildRuns作成部分は -# Apple公式ドキュメントの仕様に基づくが未検証。初回実行時はApp Store Connect側で -# 実際にビルドが開始されるか確認すること)。 -# -# 使い方: trigger-xcode-cloud-build.sh [branch_name] -# branch_nameを省略した場合は main を対象にする。 -# -# 必須環境変数(fastlane/Fastfile の ASC_KEY_ID / ASC_ISSUER_ID / ASC_API_KEY_PATH と同じ命名): -# ASC_KEY_ID App Store Connect APIキーのKey ID -# ASC_ISSUER_ID App Store Connect APIキーのIssuer ID -# ASC_API_KEY_PATH 秘密鍵ファイル(.p8)へのパス - -set -e - -API_HOST="https://api.appstoreconnect.apple.com" - -usage() { - cat <<'EOS' -Usage: trigger-xcode-cloud-build.sh [branch_name] - - workflow_id Xcode CloudワークフローのID(App Store Connect上のワークフロー詳細画面URLに含まれるID) - branch_name ビルド対象のgit branch名(省略時は main) - -Env: - ASC_KEY_ID App Store Connect APIキーのKey ID (必須) - ASC_ISSUER_ID App Store Connect APIキーのIssuer ID (必須) - ASC_API_KEY_PATH 秘密鍵ファイル(.p8)へのパス (必須) -EOS -} - -check_requirements() { - for cmd in curl jq openssl python3; do - if ! command -v "$cmd" &> /dev/null; then - echo "エラー: ${cmd} が見つかりません" >&2 - exit 1 - fi - done - if [ -z "${ASC_KEY_ID:-}" ]; then - echo "エラー: 環境変数 ASC_KEY_ID が未設定です" >&2 - exit 1 - fi - if [ -z "${ASC_ISSUER_ID:-}" ]; then - echo "エラー: 環境変数 ASC_ISSUER_ID が未設定です" >&2 - exit 1 - fi - if [ -z "${ASC_API_KEY_PATH:-}" ] || [ ! -f "$ASC_API_KEY_PATH" ]; then - echo "エラー: 環境変数 ASC_API_KEY_PATH が未設定、またはファイルが存在しません" >&2 - exit 1 - fi -} - -base64url() { - openssl base64 -A | tr '+/' '-_' | tr -d '=' -} - -# ES256でJWTを生成し、変数 JWT にセットする(scripts/appstoreconnect.shと同一実装) -generate_jwt() { - local now exp header_b64 payload_b64 signing_input der_sig_file sig_b64 - - now=$(date +%s) - exp=$((now + 1190)) - - header_b64=$(printf '{"alg":"ES256","kid":"%s","typ":"JWT"}' "$ASC_KEY_ID" | base64url) - payload_b64=$(printf '{"iss":"%s","iat":%d,"exp":%d,"aud":"appstoreconnect-v1"}' "$ASC_ISSUER_ID" "$now" "$exp" | base64url) - signing_input="${header_b64}.${payload_b64}" - - der_sig_file=$(mktemp "${TMPDIR:-/tmp}/asc_jwt_sig.XXXXXX") - printf '%s' "$signing_input" | openssl dgst -sha256 -sign "$ASC_API_KEY_PATH" -out "$der_sig_file" - - # ES256のJWT署名はDER(ASN.1)ではなくraw r||s(32byte+32byte)が必要なため変換する - sig_b64=$(python3 - "$der_sig_file" <<'PY' -import sys, base64 - -with open(sys.argv[1], "rb") as f: - der = f.read() - - -def read_len(data, idx): - length = data[idx] - idx += 1 - if length & 0x80: - n = length & 0x7F - length = int.from_bytes(data[idx:idx + n], "big") - idx += n - return length, idx - - -def read_int(data, idx): - assert data[idx] == 0x02 - idx += 1 - length, idx = read_len(data, idx) - val = data[idx:idx + length] - idx += length - return val, idx - - -idx = 1 # SEQUENCEタグをskip -_, idx = read_len(der, idx) -r, idx = read_int(der, idx) -s, idx = read_int(der, idx) -r = r.lstrip(b"\x00").rjust(32, b"\x00") -s = s.lstrip(b"\x00").rjust(32, b"\x00") -raw = r + s -sys.stdout.write(base64.urlsafe_b64encode(raw).rstrip(b"=").decode()) -PY -) - rm -f "$der_sig_file" - - JWT="${signing_input}.${sig_b64}" -} - -# call [BODY] -call() { - local method="$1" - local path="$2" - local body="${3:-}" - local url="${API_HOST}${path}" - local response http_status response_body - - generate_jwt - - if [ -n "$body" ]; then - response=$(curl -sS -w '\n%{http_code}' \ - -X "$method" "$url" \ - -H "Authorization: Bearer ${JWT}" \ - -H "Content-Type: application/json" \ - -d "$body") - else - response=$(curl -sS -w '\n%{http_code}' \ - -X "$method" "$url" \ - -H "Authorization: Bearer ${JWT}") - fi - - http_status=$(echo "$response" | tail -n1) - response_body=$(echo "$response" | sed '$d') - - if [ "$http_status" -lt 200 ] || [ "$http_status" -ge 300 ]; then - if [ -n "$response_body" ]; then - echo "$response_body" | jq . >&2 - fi - echo "エラー: HTTP ${http_status}" >&2 - exit 1 - fi - - if [ -n "$response_body" ]; then - echo "$response_body" - fi -} - -main() { - local workflow_id="${1:?usage: $0 [branch_name]}" - local branch_name="${2:-main}" - - check_requirements - - echo "Resolving repository for workflow ${workflow_id}..." >&2 - local workflow_response repository_id - workflow_response=$(call GET "/v1/ciWorkflows/${workflow_id}?include=repository") - repository_id=$(echo "$workflow_response" | jq -r '.data.relationships.repository.data.id // empty') - - if [ -z "$repository_id" ]; then - echo "エラー: workflow ${workflow_id} に紐づくscmRepositoryが見つかりませんでした" >&2 - exit 1 - fi - echo "✓ repository_id=${repository_id}" >&2 - - echo "Resolving git reference for branch ${branch_name}..." >&2 - local git_refs_response git_reference_id - git_refs_response=$(call GET "/v1/scmRepositories/${repository_id}/gitReferences?filter[name]=${branch_name}&limit=1") - git_reference_id=$(echo "$git_refs_response" | jq -r '.data[0].id // empty') - - if [ -z "$git_reference_id" ]; then - echo "エラー: branch ${branch_name} のgitReferenceが見つかりませんでした" >&2 - exit 1 - fi - echo "✓ git_reference_id=${git_reference_id}" >&2 - - echo "Starting Xcode Cloud build..." >&2 - local body build_response build_id build_number - body=$(jq -n --arg workflow_id "$workflow_id" --arg ref_id "$git_reference_id" \ - '{data:{type:"ciBuildRuns",relationships:{ - workflow:{data:{type:"ciWorkflows",id:$workflow_id}}, - sourceBranchOrTag:{data:{type:"scmGitReferences",id:$ref_id}} - }}}') - build_response=$(call POST "/v2/ciBuildRuns" "$body") - build_id=$(echo "$build_response" | jq -r '.data.id // empty') - build_number=$(echo "$build_response" | jq -r '.data.attributes.number // empty') - - if [ -z "$build_id" ]; then - echo "エラー: ビルド開始のレスポンスにidが含まれていませんでした" >&2 - echo "$build_response" | jq . >&2 - exit 1 - fi - - echo "✓ Xcode Cloud build started: id=${build_id} number=${build_number}" -} - -if [ "$#" -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - usage - exit 1 -fi - -main "$@"