ci: publish regtest apk on pr builds - #1241
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Could be triggered by a GH CI action to only spend resources when intended |
That's what I was prompting for, but the simpler path is hooking into the existing CI workflow: it already builds assembleDevDebug on every PR push, so this adds no build, only a 139 MB artifact upload (free on public repos) and a few-second comment job. A manual trigger would bring back the "someone has to dispatch it" step this PR removes. |
Got it, so the resource is already used anyway |
jvsena42
left a comment
There was a problem hiding this comment.
The security shape of this is right, and I checked the parts that usually go wrong on a publish-artifact workflow:
- Trigger is
pull_request, notpull_request_target(:6), so fork PRs get a read-only token and no secrets — no fork privilege-escalation path. - The privileged
apk-commentjob runs no contributor code: noactions/checkout, an inlinegithub-scriptbody, gated onhead.repo.fork == false(:83), andHEAD_SHA/ARTIFACT_URLpassed viaenv:and read throughprocess.envrather than template-interpolated — so no script injection. - The artifact really is regtest:
assembleDevDebug→devflavor →BuildConfig.NETWORK = "REGTEST"(app/build.gradle.kts:87,:333-337),.devsuffix, "Bitkit Regtest" name. No mainnet variant can reach it. - No signing-key exposure —
debuguses the checked-indebug.keystorewith public credentials; the release keystore is a separate config this job never touches.GOOGLE_SERVICES_JSON_BASE64in a publisheddevDebugAPK is pre-existing (e2e.yml:69,:86-89already does it on every non-draft PR), not introduced here.CHATWOOT_APIisn't consumed by any Gradle or Kotlin source, so it isn't baked in.
Requesting changes on one thing that will land after merge rather than here — details inline.
requested re-review; former changes addressed
jvsena42
left a comment
There was a problem hiding this comment.
Reviewed as a supply-chain surface, since this publishes a downloadable artifact built from PR code. No HIGH, no MEDIUM. One LOW inline, and it's arguably out of scope — your call.
The things that would have been serious, and why they aren't:
- Trigger is
pull_request(:6, types[opened, synchronize, reopened, ready_for_review]). Notpull_request_target, noworkflow_run. The newapk-commentjob adds no trigger of its own and additionally gates ongithub.event_name == 'pull_request'(:86). No untrusted-code-with-secrets path. - Fork PRs cannot reach secrets. Repo is public, so under
pull_requestfrom a forksecrets.*resolve empty andGITHUB_TOKENis read-only. Concretely:GOOGLE_SERVICES_JSON_BASE64(:54) empty → the decode step'sif [ -z ... ]; then ... exit 0(:47-50) takes the placeholder path instead of failing;CHATWOOT_API(:61) empty. Andapk-commentis hard-gated onhead.repo.fork == false(:86), so thepull-requests: writejob never runs for a fork at all. Nothing in the diff makes a fork PR newly hang or fail. - Permissions narrow rather than widen. There is no workflow-level
permissions:block, so adding job-level blocks can only reduce.buildgetscontents: read+packages: read(:24-26);apk-commentgets onlypull-requests: write(:89-90) — nocontents, and it runs noactions/checkout, so it never materialises PR code. The greptile P1 (writable token reaching PR-controlled Gradle) is genuinely resolved by the split: the token handed to Gradle at:60now lives in a job whose ceiling is read-only. Run34413156506at head8688216showspull-requests: writealone is sufficient forpaginate(listComments)+createComment. - No untrusted interpolation. Grepped
${{across the wholegithub-scriptbody (:99-120) — zero hits. The only two interpolations in the new job are inenv:(:96,:97) and read back asprocess.env.*, never template-expanded into shell or script context. Neither is attacker-controlled:artifact-urlis emitted byactions/upload-artifactitself (PR-authored Gradle can only write to its own step's$GITHUB_OUTPUTunder idbuild), andhead.shais 40 hex chars. Nopull_request.title/body/head.ref/user.loginanywhere in the file. The new job has norun:block at all. - The artifact really is regtest —
assembleDevDebug→devflavour →.devapplicationId suffix,BuildConfig.NETWORK = "REGTEST". Debug signing uses the checked-indebug.keystorewith public credentials; the release keystore is a separate config this job never references. No mainnet variant is reachable from this workflow. - Artifact hygiene.
bitkit-dev-debug-apk_${{ github.run_number }}(:79) is unique per run, so concurrent PRs can't collide, and it's distinct fromunit_test_report_...and e2e.yml'sbitkit_e2e.apk.retention-days: 30(:81) matches the "expires in 30 days" text at:108. - Skip paths are correct. Draft →
buildskipped → null artifact-url →apk-commentskipped. Build fails → upload skipped → empty output → no comment. Push tomaster→ blocked by the event check.
Not raised, because they're settled or produce no wrong outcome: the sticky-comment staleness question (the body carries head sha + run link, which reads as the intended mitigation); if: always() at job level surviving a cancel-in-progress cancellation (traced it — the cancelled run can only comment when its build already succeeded, and the superseding run's comment lands after, so the final PR state is still the newest commit; !cancelled() would be tidier but changes no outcome). if-no-files-found: error and packages: read are both the outcomes of my earlier threads and are exactly the shape I asked for.
jvsena42
left a comment
There was a problem hiding this comment.
Fix confirmed at f85f88f4, and you went further than I asked — I think correctly.
I'd suggested only adding if: always() to Upload test report so a red test run still yields a report. You did that (:68, now always() && steps.build.outcome == 'success') and dropped the condition from Upload APK, so it falls back to the implicit success() and no APK is published from a run whose tests failed. That's the better call: the asymmetry I flagged was "the wrong artifact survives a red run", and withholding the APK entirely resolves it more cleanly than publishing both.
Checked the knock-on effects:
- Test report on a red run: build succeeds → tests fail → job status is failure →
always()keeps the step alive, andsteps.build.outcome == 'success'still holds, soapp/build/reports/tests/testDevDebugUnitTest/uploads. If the build fails there are no tests to report, and the guard correctly skips. That's the case I raised. apk-commentlosingalways()(:86) is consistent, not a regression.artifact-urlis only non-empty whenUpload APKran, which now requires the job to have succeeded — soneeds.build.outputs.artifact-url != ''alone can never be true on a failed build. Droppingalways()also removes the cancellation edge I'd noted as cosmetic last round, where a cancelled run could still comment. Now it can't.- Draft and fork paths unchanged: draft →
buildskipped → null output → job skipped; fork still hard-gated onhead.repo.fork == false.
Nothing else in the two commits. That was my only finding on this PR — clean from my side.
jvsena42
left a comment
There was a problem hiding this comment.
Re-checked at b7a3e1a6. The only delta since I confirmed the fix is the master merge — git diff f85f88f4 b7a3e1a6 -- .github/workflows/ci.yml is empty, so the reviewed content is byte-identical and git show --cc shows no conflict resolution in that file.
The PR's own delta against master is still just the +56-line APK job. Nothing to re-review; my previous confirmation stands.
One note, not a finding: this branch merged a master that predates #1216, so git diff origin/master b7a3e1a6 currently shows the Paykit issuer-interop files as deletions. That's just being behind, not a removal — mergeable=MERGEABLE, so it'll resolve on the next sync or at merge time. Flagging only so the diff view doesn't alarm anyone reading it.
jvsena42
left a comment
There was a problem hiding this comment.
Re-checked at 820e73c2. Master merge only — git diff b7a3e1a6 820e73c2 -- .github/workflows/ci.yml is empty, and git show --cc 820e73c2 has no combined-diff lines, so nothing was resolved by hand.
The PR's own delta against master is now exactly .github/workflows/ci.yml | 56 ++++ and nothing else — the Paykit files that showed as deletions last time are gone from the diff, as expected once this branch caught up with #1216. That note is resolved.
Nothing to re-review; my confirmation stands.
|
Just to note every e2e run already has staging regtest apk as artifact, see: e.g. https://github.com/synonymdev/bitkit-android/actions/runs/34602699398 (Artifacts: bitkit-e2e-apk-regtest_4563) |
Closes #1240
This PR:
masterDescription
assembleDevDebugwith the realgoogle-services.jsonfromGOOGLE_SERVICES_JSON_BASE64, so the artifact receives Firebase notifications like the E2E builds.bitkit-dev-debug-apk_<run_number>, only the*-universal.apkfile is uploaded.apk-commentjob holdspull-requests: writeand posts the comment from the build job's artifact URL output, so PR-controlled Gradle code never sees a writable token.Preview
N/A — no user-visible changes.
QA Notes
Manual Tests
(after merge)Open a PR with a code change → CI run:bitkit-dev-debug-apk_<run>artifact appears and oneRegtest APKcomment links to it.(after merge)Push again to the same PR: the same comment updates with the new SHA and artifact link, no second comment.(after merge)Push a commit with a failing unit test: no APK artifact and no comment update, the unit test report is still uploaded.(after merge)Merge tomaster→ CI run for the merge commit: artifact present, no comment attempted.(after merge)Install the artifact on a regtest device → receive a push notification: it arrives.Automated Checks
actionlint .github/workflows/ci.ymlpasses locally.(after merge).🤖 Generated with Claude Code