First, your question — what is actually verified?
You asked how we can fully test the pipeline when the consoles are not configured. Fair challenge. Here is the precise answer.
| Half of the pipeline | State | Why |
|---|---|---|
| Build — source to a signed, correctly-versioned package | VERIFIED | Ran end to end today from a clean checkout. 5 minutes, 8 of 8 gates, signature and versionCode read back out of the artifacts. This half is genuinely finished and needs nothing from Google. |
| Deliver — package to a tester's phone, and signing in on it | BLOCKED ON YOU | Nobody can verify this yet, including me. It cannot be faked or simulated — it requires a real Play app and a real OAuth client, both of which need your Google account. |
~/.hoop/wt-android-verify/app/build/app/outputs/flutter-apk/app-release.apk, versionCode 1, signed with our real upload key. Copy it to an Android phone and install it. Everything works except Google sign-in. That confirms the built package is genuinely good before you spend time in the consoles.What you are setting up, in order
| Part | Where | What it unblocks |
|---|---|---|
| A | Google Cloud Console | Google sign-in working on Android |
| B | Play Console | Getting the app to testers |
| C | Both | Uploads without a human — optional, do it last |
| D | A phone | Proving all of it actually works |
A and B are independent — either order. C must come after B (the service account is granted access to an app that has to exist first). One step inside A can only be done after B; it is flagged where it appears.
AGoogle Cloud Console — make Google sign-in work
Android sign-in fails today with ApiException: 10 (DEVELOPER_ERROR). Google identifies an Android app by the pair package name + signing certificate SHA-1, and that pair has never been registered. There is no code to change — the app's sign-in code already works on Android.
A1 · Get into the right project
Go to console.cloud.google.com and select project number 584364169898.
For reference, the Web client already there is 584364169898-mhmfq7c5…. Our backend validates every Google login against that exact ID, so the Android client must live in the same project.
A2 · OAuth consent screen
It should already be configured — you cannot create OAuth clients without it, and two already exist. Check APIs & Services → OAuth consent screen.
A3 · Create the Android OAuth client
APIs & Services → Credentials → + Create Credentials → OAuth client ID → Android.
| Field | Value — copy exactly |
|---|---|
| Name | HOOP Android (upload key) — internal label, anything works |
| Package name | com.hooptech.hoop |
| SHA-1 | 7A:03:8F:8C:9A:FA:D9:26:A5:62:CD:B9:3E:77:29:E2:25:E7:91:CA |
Click Create. Nothing needs copying back — no client ID goes into our code. Android finds it by package and fingerprint.
A4 · You will need up to three of these, one per signing key
This is the part that catches people. Re-signing an app changes its SHA-1, so as far as Google is concerned each differently-signed build is a different app.
| Key | When to add it | SHA-1 |
|---|---|---|
| Upload key | Now (A3 above) | 7A:03:8F:8C:9A:FA:D9:26:A5:62:CD:B9:3E:77:29:E2:25:E7:91:CA Covers every package our build produces and installs directly. |
| Debug key | Only if testing debug builds | 89:9C:99:50:3C:2A:D1:77:63:82:11:37:FB:9A:C3:2C:FD:7F:D0:9A This Mac's debug key. Machine-specific. |
| Play signing key | After part B | Does not exist yet — Google generates it when you enable Play App Signing. Get it from Play Console → Test and release → Setup → App signing, then repeat A3 with that fingerprint. |
✅ Verifying part A
There is no "configured" indicator in the console. The only real test is signing in on a phone, with the log open:
~/Library/Android/sdk/platform-tools/adb logcat | grep google_signin
| What you see | Means |
|---|---|
| Sign-in completes, nothing logged | Working. |
code=sign_in_failed + a DEVELOPER_ERROR line | Pair still unregistered — wrong project, typo in the fingerprint, or the installed build was signed with a different key than the one you registered. |
登录成功但 idToken 为空 | Different problem, not the Android client. Tell me if you see this. |
Allow a few minutes after creating the client, and force-stop the app before retrying.
BPlay Console — get the app to testers
B1 · Create the app
play.google.com/console → Create app. Name (what users see in the store), default language, App not Game, and Free.
B2 · Play App Signing — the one permanent choice
With it on there are two keys: yours (the upload key in
~/.hoop-keys/) and Google's (the app signing key, which they hold). You sign, Google verifies, strips your signature and re-signs with theirs.Why it matters: if the upload key is ever lost or leaked, you can register a replacement and keep shipping. Without it, losing that one file permanently ends the app — Android only accepts updates signed with the original key, there is no appeal, and the only path forward is a new package name with all installs and ratings abandoned.
The cost is that Google holds a key you cannot export. Against permanently losing the app, that is not a close call.
B3 · The declarations Play requires
Under Policy → App content. Internal testing does not require these — you can get a build to testers first. They are required before any public track, and some take review time, so start them early.
| Item | Notes |
|---|---|
| Privacy policy URL | Ready to paste: https://hoopcomm.com/privacy — confirmed returning HTTP 200 today. |
| Data safety | What the app collects and why. HOOP collects account identifiers, contacts (invite feature), photos and camera, and messages. Answer accurately — it is cross-checked against actual app behaviour and inaccuracy gets apps removed. |
| Content rating | Questionnaire; the rating is issued from your answers. Social features and user-generated content are the material part. |
| Target audience | Including children triggers a large extra policy surface. Decide deliberately. |
| Ads | The app does contain ad code (play-services-ads is a dependency). Declare it. |
| Store listing | Descriptions, icon 512×512, feature graphic 1024×500, ≥2 phone screenshots. Screenshots can come from a sideloaded build. |
B4 · Upload to internal testing
Test and release → Testing → Internal testing → Create new release.
Upload the .aab. One already exists from this morning's build:
~/.hoop/wt-android-verify/app/build/app/outputs/bundle/release/app-release.aab
# versionCode 1, signed with our upload key, 531M
Or have NOVA cut a fresh one — either works, the pipeline is the same.
Then release notes → Save → Review release → Start rollout to Internal testing.
B5 · Add testers
Testers tab → create an email list → add each tester's Google account address (up to 100) → copy the opt-in link Play generates.
B6 · Go back and finish part A
Now that Play App Signing is on, Google has generated its own key. Get its SHA-1 from Test and release → Setup → App signing and repeat A3 with it. Sign-in will fail for everyone installing from Play until you do.
✅ Verifying part B
- The release shows as Available to internal testers, not stuck in review or draft.
- Play's App signing page shows both an app signing certificate and an upload certificate — the upload one must match our SHA-1
7A:03:…:91:CA. If it does not, the package was signed with the wrong key. - A tester on the list can find and install the app from Play.
CService account — uploads without a human
Optional, and do it last. Nothing above depends on it. It lets the build pipeline push to Play automatically instead of someone dragging a file into a browser.
C1 · Enable the API
Cloud Console, project 584364169898 → APIs & Services → Library → find Google Play Android Developer API → Enable.
C2 · Create the service account and its key
IAM & Admin → Service Accounts → Create service account. Name it something obvious like hoop-play-publisher. No project roles are needed — the permissions that matter are granted in Play, not here.
Then open it → Keys → Add key → Create new key → JSON. It downloads once.
mv ~/Downloads/<whatever>.json ~/.hoop-keys/play-service-account.json chmod 600 ~/.hoop-keys/play-service-account.jsonNever in the repository, never pasted into chat, never emailed.
C3 · Grant it access in Play
Play Console → Users and permissions → Invite new users → paste the service account's email (it looks like hoop-play-publisher@<project>.iam.gserviceaccount.com, and is the client_email field inside the JSON) → grant release permissions for this app → Invite user.
✅ Verifying part C — an actual command, not an inspection
Nothing in either console tells you the three pieces fit together. This does:
cd <a checkout of the repo> python3 tools/tf-build/verify_play_credentials.py \ ~/.hoop-keys/play-service-account.json com.hooptech.hoop
It uses the credential to open a Play "edit" and then abandons it — the only action that proves genuine release permission, and it changes nothing (an uncommitted edit simply expires). Read-only calls cannot prove this, because read access does not imply publish access.
| Result | Meaning |
|---|---|
| ✅ 200 | All three pieces are correct. You are done. |
| 🛑 401 | The credential itself was rejected — wrong project, revoked, or expired key. |
| 🛑 403 | Valid credential, no permission. Either C1 or C3 is missing. |
| 🛑 404 | No such app on Play. Part B is not done. |
DThe end-to-end test — this is the moment it is proven
Do these in order. Each step depends on the one before, so the first failure tells you exactly which part is wrong.
| # | Do | Pass looks like |
|---|---|---|
| 1 | Have NOVA build, or reuse this morning's AAB | Log ends ✅ versionCode N,真钥匙签的 |
| 2 | Upload it to internal testing (B4) | Play accepts it and reports a download size |
| 3 | Check the upload certificate (B, verify) | Play shows upload SHA-1 = 7A:03:…:91:CA |
| 4 | Register the Play signing SHA-1 (B6 → A3) | A third Android OAuth client exists |
| 5 | A tester opts in and installs from Play | App appears in Play for them and installs |
| 6 | Open it and sign in with Google | Sign-in succeeds; nothing in adb logcat | grep google_signin |
| 7 | Use the app — send a message, open a game | Normal behaviour, same as iOS |
Step 5 or 6 failing is expected the first time if part A's third fingerprint was skipped. That is the single most likely failure, and A4 is the fix.
Checklist
| Done when | Check | |
|---|---|---|
| A1 | In project 584364169898 | iOS and Web OAuth clients visible |
| A2 | Consent screen configured | If "Testing", every tester is on the test-user list |
| A3 | Android client for the upload key | Sign-in works on a sideloaded build |
| B1 | App created | Appears in Play Console |
| B2 | Play App Signing ON | App signing page lists two certificates |
| B4 | AAB uploaded | Release is live on internal testing; size noted |
| B5 | Testers added | A tester can install from Play |
| B6 | Android client for the Play signing key | Sign-in works on a Play-installed build |
| C1-C3 | Service account (optional) | verify_play_credentials.py prints ✅ |
| D | End-to-end | Step 6 passes |
What I verified, and what I did not
Verified on this machine today: both fingerprints, read out of the keystores with keytool. The package name and the Web client ID, cross-checked between build.gradle.kts, google_signin.dart and the backend config. The privacy URL returning 200. The signed AAB and APK referenced here — they exist, at those paths, with those sizes. The failure paths of the verification script.
Not verified — instructions, not observations: every console screen in parts A, B and C. I have no access to either console, so menu names and layouts come from Google's documentation and may have moved. The values to enter are verified from our own code and keys; the clicks are not. Also unverified: the success path of the credential script, whether Play accepts our size, and whether sign-in works once the client exists — that is exactly what part D is for.
Companion: the build pipeline (verified working) · the original wireframe.
先回答你那个问题:到底哪一半验过了?
你问:两个控制台都还没配,凭什么说流水线验过了。这个问题问得对,答案是这样的。
| 流水线的哪一半 | 状态 | 凭什么 |
|---|---|---|
| 出包 —— 从源码到一个签好名、版本号正确的包 | 已验证 | 今天在干净检出上端到端真跑过:5 分钟、八道闸全过、签名和版本号都是从产物里反读的。这一半是真做完了,而且完全不需要 Google。 |
| 送达 —— 从包到测试者手机上,并且能登录 | 卡在你这儿 | 现在谁都验不了,包括我。 这件事没法模拟、没法造假 —— 它需要一个真的 Play 应用和一个真的 OAuth 客户端,两样都要你的 Google 账号。 |
~/.hoop/wt-android-verify/app/build/app/outputs/flutter-apk/app-release.apk,版本号 1,用我们的真上传密钥签的。拷到安卓手机上装起来,除了 Google 登录之外一切都能用。先这么确认包本身是好的,再去花时间配控制台。你要配的东西,按顺序
| 部分 | 在哪儿配 | 配完解开什么 |
|---|---|---|
| A | Google Cloud Console | 安卓上的 Google 登录 |
| B | Play Console | 把包送到测试者手里 |
| C | 两边都要 | 不用人手也能上传 —— 可选,放最后 |
| D | 一台手机 | 证明上面这些真的成了 |
A 和 B 互相独立,先做哪个都行。C 必须在 B 之后(服务账号是被授权去操作一个应用,那个应用得先存在)。A 里面有一步只能等 B 做完才能做,到那儿会点明。
AGoogle Cloud Console —— 让 Google 登录能用
安卓上现在登录会失败,报 ApiException: 10(DEVELOPER_ERROR)。Google 靠包名 + 签名证书 SHA-1 这一对来认一个安卓应用,而这一对从来没登记过。代码一个字都不用改 —— 应用里那段登录代码在安卓上本来就是对的。
A1 · 先进对项目
打开 console.cloud.google.com,选项目编号 584364169898。
作参考:已经在那儿的 Web 客户端是 584364169898-mhmfq7c5…。我们后端验每一次 Google 登录时认的就是这个 ID,所以安卓客户端必须建在同一个项目里。
A2 · OAuth 同意屏幕
它应该早就配好了 —— 没有它就建不出 OAuth 客户端,而那儿已经有两个了。去 API 和服务 → OAuth 同意屏幕 看一眼。
A3 · 建安卓 OAuth 客户端
API 和服务 → 凭据 → 创建凭据 → OAuth 客户端 ID → 类型选 Android。
| 填什么 | 值 —— 原样照抄 |
|---|---|
| 名称 | HOOP Android (upload key) —— 只是给自己看的标签,随便起 |
| 包名 | com.hooptech.hoop |
| SHA-1 | 7A:03:8F:8C:9A:FA:D9:26:A5:62:CD:B9:3E:77:29:E2:25:E7:91:CA |
点创建。不用把任何东西抄回来 —— 没有任何客户端 ID 要进我们的代码,安卓是靠包名和指纹自己找到它的。
A4 · 一共要建三个,每把签名密钥一个
这是最容易栽的地方。重新签名会让 SHA-1 变掉,所以在 Google 眼里,签名不同的包就是不同的应用。
| 哪把密钥 | 什么时候加 | SHA-1 |
|---|---|---|
| 上传密钥 | 现在(就是上面 A3) | 7A:03:8F:8C:9A:FA:D9:26:A5:62:CD:B9:3E:77:29:E2:25:E7:91:CA 管住我们自己出的、自己装的每一个包。 |
| 调试密钥 | 只有要测调试包才加 | 89:9C:99:50:3C:2A:D1:77:63:82:11:37:FB:9A:C3:2C:FD:7F:D0:9A 这台 Mac 的调试密钥,每台机器都不一样。 |
| Play 签名密钥 | 等 B 做完之后 | 现在还不存在 —— 打开 Play 应用签名时 Google 自己生成。之后去 Play Console → 测试和发布 → 设置 → 应用签名 拿到它,再按 A3 建一个。 |
✅ 怎么验 A 这一部分
控制台里没有任何地方会显示「配好了」。唯一真的检验就是在手机上登一次,同时开着日志:
~/Library/Android/sdk/platform-tools/adb logcat | grep google_signin
| 你看到什么 | 说明 |
|---|---|
| 登录成功,日志里什么都没有 | 成了。 |
code=sign_in_failed 加一行 DEVELOPER_ERROR | 这一对还是没登记上 —— 项目不对、指纹敲错了、或者装的那个包不是你登记的那把密钥签的。 |
登录成功但 idToken 为空 | 那是另一个问题,不是安卓客户端这儿。看到这个告诉我。 |
建完客户端等几分钟才生效,重试前先把应用强制停止一次。
BPlay Console —— 把包送到测试者手里
B1 · 建应用
play.google.com/console → 创建应用。名字(用户在商店里看到的那个)、默认语言、类型选应用不是游戏、选免费。
B2 · Play 应用签名 —— 唯一那个永久性选择
打开之后有两把密钥:你的(
~/.hoop-keys/ 里那把上传密钥)和 Google 的(真正的应用签名密钥,它保管)。你签,Google 验,然后把你的签名剥掉、换成它自己的。为什么重要:上传密钥万一丢了或者泄露了,你还能申请换一把,继续发版。不打开的话,丢掉那一个文件就是这个应用的终点 —— 安卓只认最初那把密钥签的更新包,没有申诉途径,唯一的出路是换个包名重新上架,已有的安装量和评分全部作废。
代价是 Google 手上有一把你导不出来的密钥。和「永久失去这个应用」比,这根本不用犹豫。
B3 · Play 要你填的那几份表
在 政策 → 应用内容 底下。内部测试不需要这些 —— 可以先把包发给测试者。但公开发布之前全都要,而且有几份要审核时间,所以早点开始。
| 哪一份 | 说明 |
|---|---|
| 隐私政策地址 | 现成的,直接粘: https://hoopcomm.com/privacy —— 今天核过,返回 200。 |
| 数据安全 | 声明应用收集什么、为什么收。HOOP 收账号标识、通讯录(邀请功能)、照片和相机、消息内容。如实填 —— 它会和应用的真实行为对照,填不实会被下架。 |
| 内容分级 | 一份问卷,分级是按你的答案自动出的。社交功能和用户生成内容是关键那几题。 |
| 目标受众 | 答案里一旦包含儿童,会触发一大片额外的政策要求。这一题要认真想。 |
| 广告声明 | 应用里确实有广告代码(依赖里有 play-services-ads),照实声明。 |
| 商店页面 | 长短描述、图标 512×512、特色图 1024×500、至少两张手机截图。截图可以拿自己装的包来截。 |
B4 · 传到内部测试
测试和发布 → 测试 → 内部测试 → 创建新版本。
把 .aab 传上去。今早那次出包的那个还在:
~/.hoop/wt-android-verify/app/build/app/outputs/bundle/release/app-release.aab
# 版本号 1,我们的上传密钥签的,531M
或者让 NOVA 现出一个新的 —— 都行,走的是同一条流水线。
然后填更新说明 → 保存 → 检查版本 → 开始向内部测试发布。
B5 · 加测试者
测试者那一栏 → 建一份邮箱清单 → 把每个测试者的 Google 账号邮箱加进去(最多 100)→ 复制 Play 生成的那条加入链接。
B6 · 回去把 A 补完
现在 Play 应用签名开了,Google 已经生成了它自己那把密钥。去 测试和发布 → 设置 → 应用签名 拿到它的 SHA-1,按 A3 再建一个客户端。不做这一步,所有从 Play 装的人都登不了。
✅ 怎么验 B 这一部分
- 版本状态是「可供内部测试人员使用」,不是卡在审核或草稿。
- Play 的应用签名页面上两张证书都在 —— 应用签名证书,和上传证书。上传那张必须等于我们的
7A:03:…:91:CA。对不上就说明这个包是用错的密钥签的。 - 名单里的测试者能在 Play 里找到并装上这个应用。
C服务账号 —— 不用人手也能上传
可选,而且放到最后做。 上面每一步都不依赖它。它的作用是让出包流水线自动把包推上 Play,而不是让人去浏览器里拖一个文件。
C1 · 启用那个 API
Cloud Console,项目 584364169898 → API 和服务 → 库 → 搜 Google Play Android Developer API → 启用。
C2 · 建服务账号,给它一把密钥
IAM 和管理 → 服务账号 → 创建服务账号。 名字起得一眼能认,比如 hoop-play-publisher。不用给它任何项目角色 —— 真正管用的权限是在 Play 那边给的,不是这儿。
建完点进去 → 密钥 → 添加密钥 → 创建新密钥 → JSON。它只会下载这一次。
mv ~/Downloads/<那个文件>.json ~/.hoop-keys/play-service-account.json chmod 600 ~/.hoop-keys/play-service-account.json永远不进仓库,永远不粘进聊天,永远不发邮件。
C3 · 在 Play 里给它权限
Play Console → 用户和权限 → 邀请新用户 → 粘那个服务账号的邮箱(长得像 hoop-play-publisher@<项目>.iam.gserviceaccount.com,就是 JSON 里 client_email 那个字段)→ 给它这个应用的发布权限 → 邀请用户。
✅ 怎么验 C —— 一条真能跑的命令,不是"看一眼"
两个控制台里都没有任何地方会告诉你这三块拼上了。这条命令会:
cd <仓库的任意一棵检出> python3 tools/tf-build/verify_play_credentials.py \ ~/.hoop-keys/play-service-account.json com.hooptech.hoop
它拿这把凭据去开一个 Play 的 edit 然后丢掉 —— 那是唯一能证明「真有发布权限」的动作,而且什么都不会改(不提交的 edit 会自己过期)。只读接口证明不了这件事,因为有读权限不等于能发版。
| 结果 | 说明 |
|---|---|
| ✅ 200 | 三块全对,C 部分完成。 |
| 🛑 401 | 凭据本身没被接受 —— 项目不对、被撤销、或者过期了。 |
| 🛑 403 | 凭据有效但没权限。C1 或者 C3 漏了一个。 |
| 🛑 404 | Play 上没这个应用 —— B 部分还没做。 |
D端到端测试 —— 到这一步才算被证明
按顺序做。每一步都依赖上一步,所以第一个失败的地方就直接指出是哪一块配错了。
| # | 做什么 | 过了长什么样 |
|---|---|---|
| 1 | 让 NOVA 出包,或者直接用今早那个 AAB | 日志最后一行是 ✅ versionCode N,真钥匙签的 |
| 2 | 传到内部测试(B4) | Play 收下了,并报出一个下载体积 |
| 3 | 核一眼上传证书(B 的验证那节) | Play 上显示的上传 SHA-1 = 7A:03:…:91:CA |
| 4 | 登记 Play 签名密钥的 SHA-1(B6 → A3) | 安卓 OAuth 客户端变成三个 |
| 5 | 测试者加入,并从 Play 装 | 他在 Play 里看得到这个应用,并装上了 |
| 6 | 打开应用,用 Google 登录 | 登录成功;adb logcat | grep google_signin 里什么都没有 |
| 7 | 正常用一下 —— 发条消息、开个游戏 | 和 iOS 上表现一样 |
第一次跑,第 5 或第 6 步失败是意料之中的,只要 A 里那第三个指纹没登记。那是最可能的失败,修法就是 A4。
对照清单
| 什么时候算做完 | 怎么核 | |
|---|---|---|
| A1 | 进了项目 584364169898 | 看得到 iOS 和 Web 两个 OAuth 客户端 |
| A2 | 同意屏幕配好了 | 如果是「测试中」,每个测试者都在测试用户名单里 |
| A3 | 上传密钥的安卓客户端建好了 | 自己装的包上能用 Google 登录 |
| B1 | 应用建好了 | Play Console 里能看到它 |
| B2 | Play 应用签名打开了 | 应用签名页面上有两张证书 |
| B4 | AAB 传上去了 | 内部测试上已发布;下载体积记下来了 |
| B5 | 测试者加好了 | 有人能从 Play 装上 |
| B6 | Play 签名密钥的安卓客户端也建了 | 从 Play 装的包上能用 Google 登录 |
| C1-C3 | 服务账号(可选) | verify_play_credentials.py 打出 ✅ |
| D | 端到端 | 第 6 步过了 |
哪些我真验过、哪些没有
今天在这台机器上验过的:两把指纹,都是 keytool 从密钥库里读出来的。包名和 Web 客户端 ID,在 build.gradle.kts、google_signin.dart、后端配置三处互相核过。隐私政策地址返回 200。文中提到的那个签名 AAB 和 APK —— 它们真的在那些路径上,大小也是那些。验证脚本的失败路径。
没验过的 —— 这几段是说明,不是我看到的事实:A、B、C 三部分里的每一个控制台界面。我没有这两个控制台的权限,所以菜单名和位置是按 Google 的文档写的,可能已经挪过地方。要填的值是核过的,怎么点没有。 另外没验的:凭据脚本的成功路径、Play 收不收我们这个体积、以及客户端建好之后登录是不是真的就通了 —— D 部分就是为了验这几件。