個人開発で Flutter アプリを Google Play に出すたびに、Android Studio を開いて aab をビルドし、Play Console にアップロードして、リリースノートを貼って公開ボタンを押す……という手作業を毎回やっていました。アプリが増えるとこれが地味に効いてきます。
そこで、Mac のターミナルで次の1コマンドにまとめました。
python3 store_submit.py android
fastlane の supply も使わず、Google Play Developer API(androidpublisher v3)を直接叩いて、ビルドから製品版(production トラック)へのコミットまでを通します。この記事では、その仕組みと、実際に踏んだ落とし穴を、動くコードごと全部出します。前提は Flutter(aab)です。
なぜ fastlane を使わず API を直叩きするのか
fastlane の supply でもアップロード自体はできます。ただ個人開発で使っていると、
- Ruby / gem の依存が増える
- 中で何をしているかがブラックボックスになりがち
- Google 側の API 仕様が変わったとき、自分で追従しづらい
という不満がありました。
Google Play Developer API は Python の google-api-python-client から直接叩けます。「edit を作る → aab を上げる → track を更新する → commit する」という流れがそのままコードに出てくるので、落とし穴を踏んだときに自分のコードとして直せるのが大きいです。
(App Store 側も同じ思想で App Store Connect API を直叩きしています。これは最後に触れます。)
前提となる制約:新しい個人アカウントは「12人・14日」のクローズドテストが必要
自動化の話の前に、知らないと最初に詰まる制約を書いておきます。これはコードではなく Google Play 側のアカウント要件です。
- 2023年11月13日以降に作成された個人デベロッパーアカウントは、本番(production)公開の前にクローズドテストの実施が必須になりました。
- 条件は、12人以上のテスターが14日間連続でオプトインした状態を満たすこと。これを満たすと「製品版アクセス」を申請でき、審査を通ると本番公開ができるようになります。
- 当初(2023年11月の発表時)は20人でしたが、2024年12月11日に12人へ緩和されました。
- 「14日間オプトイン」は、一度オプトインしたテスターは途中でアンインストールしてもカウントされます。
- 組織(Organization)アカウント、および2023年11月13日より前に作成された個人アカウントはこの要件の対象外で、最初から本番公開できます。
公式の要件はこちらです(数字や運用が変わることがあるので、最新は公式で確認してください)。
つまり、この記事の自動化(production トラックへのコミット)が効いてくるのは、この製品版アクセスを得たあとです。新規個人アカウントの「1本目」は、まずこのクローズドテストの壁を越える必要があります。逆に言えば、ここさえ越えてしまえば、以降のバージョンアップは1コマンドで回せます。
一番の難所:サービスアカウントの設定
Android 自動化で唯一めんどくさいのがここです。逆に言えば、一度やれば終わりです。
- GCP プロジェクトで「Google Play Android Developer API」を有効化する
- サービスアカウントを作成し、JSON 鍵をダウンロードする
- Google Play Console → ユーザーと権限で、そのサービスアカウントを招待し、リリースに必要な権限を付与する
- 権限の反映にラグがあることがある(設定直後に叩くと
403で蹴られ、数分置くと通る、ということがありました)
JSON 鍵は秘密情報なので、リポジトリには入れないでください。設定ファイルにはパスだけを書き、鍵そのものはホーム配下などに置きます。
{
"android": {
"service_account_key": "~/.config/play/service_account.json",
"package_name": "com.example.app"
}
}
提出フローのコード
実際に動かしている関数がこれです。flutter build appbundle で aab を作り、Developer API で production トラックにコミットするところまで通します。
def do_android(cfg):
version, build = get_version()
meta = cfg["metadata"]
# Pre-flight: ASO 誇張表現チェック
if not preflight_aso_check(build=build):
return False
# Pre-flight: Google Play は releaseNotes 500字上限。超過するとアップロード後に
# API error (HTTP 403) で失敗し build 時間を無駄にする。ビルド前に検出する。
CHANGELOG_LIMIT = 500
changelog_over = []
for lang, tmpl_key in [("ja-JP", "android_ja_changelog"), ("en-US", "android_en_changelog")]:
path_tmpl = meta.get(tmpl_key, "")
if not path_tmpl:
continue
path = path_tmpl.replace("{build}", build)
if os.path.exists(path):
text = read_file(path)
if text and len(text) > CHANGELOG_LIMIT:
changelog_over.append((path, len(text)))
if changelog_over:
print("❌ Google Play changelog の 500 字上限を超過:")
for p, n in changelog_over:
print(f" {p}: {n} 字 (上限 {CHANGELOG_LIMIT})")
print(" 短縮してから再実行してください。")
return False
print(f"🤖 Android 提出: {cfg['app_name']} v{version}+{build}")
print("1️⃣ flutter build appbundle...")
ok, _ = run("flutter build appbundle --release", timeout=1800)
if not ok:
return False
print("2️⃣ Google Play 製品版にアップロード...")
from google.oauth2 import service_account
from googleapiclient.discovery import build as gp_build
from googleapiclient.http import MediaFileUpload
sa_key = os.path.expanduser(cfg["android"]["service_account_key"])
package = cfg["android"]["package_name"]
creds = service_account.Credentials.from_service_account_file(
sa_key, scopes=["https://www.googleapis.com/auth/androidpublisher"]
)
service = gp_build("androidpublisher", "v3", credentials=creds)
# edit はトランザクション。commit して初めて反映される。
edit = service.edits().insert(body={}, packageName=package).execute()
edit_id = edit["id"]
aab_path = "build/app/outputs/bundle/release/app-release.aab"
media = MediaFileUpload(aab_path, mimetype="application/octet-stream")
upload = service.edits().bundles().upload(
packageName=package, editId=edit_id, media_body=media
).execute()
version_code = upload["versionCode"] # versionCode は aab から自動で返る
print(f" Version Code: {version_code}")
changelog_ja = read_file(meta.get("android_ja_changelog", "").replace("{build}", build))
changelog_en = read_file(meta.get("android_en_changelog", "").replace("{build}", build))
release_notes = []
if changelog_ja:
release_notes.append({"language": "ja-JP", "text": changelog_ja})
if changelog_en:
release_notes.append({"language": "en-US", "text": changelog_en})
service.edits().tracks().update(
packageName=package, editId=edit_id, track="production",
body={"track": "production", "releases": [{
"versionCodes": [str(version_code)],
"status": "completed",
"releaseNotes": release_notes,
}]}
).execute()
service.edits().commit(packageName=package, editId=edit_id).execute()
print(" ✅ 製品版にコミット完了")
return True
ポイントは Google Play API の edit がトランザクションになっていることです。insert で編集セッションを開き、aab のアップロードやトラック更新を積み上げて、最後に commit して初めて反映されます。途中で失敗しても commit していなければ何も変わらないので、リトライしやすい設計です。
踏んだ落とし穴
1. リリースノートは500字まで(超えるとアップロード後に弾かれる)
Google Play の releaseNotes は言語ごとに 500字上限です。超過していると、aab を上げたあとのトラック更新で HTTP 403 になって失敗します。重い flutter build appbundle を走らせたあとで弾かれると、その時間がまるごと無駄になります。
対策は、ビルドの前に changelog の文字数を数えて、超えていればその場で止めること(上のコードの Pre-flight 部分)。一番重い処理の前にチェックを置くのがコツです。
2. 1本目は手作業が必須(API だけでは完結しない)
正直に書くと、アプリの初回は API だけでは出せません。Play Console で次を手動でやる必要があります。
- アプリの作成
- ストア掲載情報(説明・スクリーンショット)
- コンテンツのレーティング・対象年齢・データセーフティの申告
- 最初のリリース(一度リリース実績を作る)
サービスアカウントによる API が効いてくるのは、アプリが「動き出したあと」です。逆に 2本目以降(バージョンアップ)は、バージョン番号を上げて1コマンドで終わります。この「最初だけ手動・あとは自動」という構造は、iOS でも同じです。
3. versionCode は aab から自動で決まる
bundles().upload のレスポンスに versionCode が入っています。これをそのままトラックに載せるので、versionCode を手で管理する必要はありません。
4. サービスアカウント権限の反映ラグ
権限を付与した直後に叩くと 403 で蹴られることがありました。設定したら数分置いてから実行すると通ります。「設定は合っているのに通らない」ときは、まずこれを疑うと早いです。
まとめ
- Mac だけで、Android Studio も CI も使わず、
flutter build appbundleから Google Play 製品版コミットまでを1コマンドにできる - 仕組みは Developer API の直叩き。難所はサービスアカウント設定だけで、一度やれば以降のリリースはバージョンを上げて実行するだけ
- 新しい個人アカウントは、本番公開の前に「12人・14日」のクローズドテストを越える必要がある(ここは自動化の外の話)
- リリースノートの500字上限と「初回は手動」だけ押さえておけば、運用でハマりにくい
おまけ:iOS(App Store)も同じ思想で1コマンドにしている
App Store 側も、fastlane を使わず App Store Connect API を直接叩いて、バージョン作成 → メタデータ同期 → ビルド → アップロード → 審査提出までを1コマンドにしています。
ただ iOS は Android より仕様のクセが多く、実運用で踏んだ落とし穴が12個ありました。公開済みバージョンへの二重提出の防止、ビルド番号の衝突、使用許諾契約の保留で VALID ビルドが現れない問題、PATCH の 409 属性ロック、暗号化コンプライアンスの自動回答、2026-05 の ASC スキーマ更新への追従……といったものです。
それらの「症状 → 原因 → 対策」と、スクリプト全文・AI エージェント用のセットアップガイド(AGENTS.md)・環境を自己診断する doctor サブコマンドまでをまとめたのが、こちらの本です。Flutter / Swift 両対応です。
Android の自動化が手に馴染んだら、iOS 側も同じやり方で一気に楽にできます。
この記事は Zenn にも掲載しています。