Skip to content

docs(billing): name the real reconnection backstop, not the one the library does not provide #304

docs(billing): name the real reconnection backstop, not the one the library does not provide

docs(billing): name the real reconnection backstop, not the one the library does not provide #304

Workflow file for this run

name: PR CI
# This workflow has no `paths` / `paths-ignore` filter at any level, and must never
# gain one. `build-and-test` below is the required status context on both the dev and
# the master ruleset, `on.pull_request.paths` filters the WHOLE workflow, and GitHub
# reports a path-skipped required check as "Expected — Waiting for status" for as long
# as the PR is open. A site-only or docs-only PR would therefore become unmergeable
# with nothing in the UI explaining why.
#
# The cost is real and will keep inviting the change: a PR that only touches web/ still
# pays the ~7 minutes of Android build, unit tests and lint below. Seven minutes of CI
# is cheaper than a PR that cannot merge. dev-check.yml, production-deploy.yml and
# web-ci.yml do filter — all three are safe to filter precisely because none of them is
# a required check.
on:
pull_request:
branches: [ "master", "dev" ]
# Least-privilege token — this workflow only reads the repo.
permissions:
contents: read
# Cancel superseded runs for the same PR.
concurrency:
group: pr-ci-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build-and-test:
name: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup JDK 21
uses: actions/setup-java@v5.6.0
with:
distribution: 'zulu'
java-version: '21'
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Assemble + unit test (foss debug)
run: ./gradlew assembleFossDebug testFossDebugUnitTest --stacktrace
# Enforced, not advisory: :app is at 0 errors / 0 warnings and app/build.gradle.kts sets
# warningsAsErrors, so lint debt can only be introduced deliberately now.
#
# Deliberately a step of build-and-test rather than a job of its own. "build-and-test" is
# the only context the dev and master rulesets require; a separate lint job would show up
# green-or-red on the PR but merge would not wait for it until someone also edited the
# ruleset. Inside this job it is required the moment this file merges.
#
# storeRelease, not fossDebug: store is the variant Play receives, and the release build
# type is where the strict detectors live. A debug-variant lint pass says very little
# about what actually ships.
- name: Android Lint (store release)
id: lint
run: ./gradlew lintStoreRelease --stacktrace
# printTextReport puts the findings in the job log, but the HTML report carries the source
# snippet and the explanation for each issue. Attach it so a red check is diagnosable from
# the PR instead of requiring a local repro of a variant nobody builds by hand.
- name: Upload lint report
if: failure() && steps.lint.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: lint-results-storeRelease
path: |
app/build/reports/lint-results-storeRelease.html
app/build/reports/lint-results-storeRelease.xml
app/build/reports/lint-results-storeRelease.txt
if-no-files-found: warn
retention-days: 14
# The only place R8 runs BEFORE a merge. It does run elsewhere — dev-check.yml:147,
# production-deploy.yml:145 and manual-build.yml:52 all shell out to fastlane, which
# assembles release — but every one of those triggers on a push to an already-merged
# branch. On the PR itself nothing minified was ever built: assembleFossDebug above does
# not minify (app/build.gradle.kts: `debug { isMinifyEnabled = false }`), lintStoreRelease
# analyses unminified classes, and codeql.yml builds with `build-mode: none`. So the first
# minified build of any change was the build that published it, and a shrinker break was
# discovered by the release rather than by review. gradle.properties runs R8 in full mode
# with android.r8.strictFullModeForKeepRules=true, which is exactly the configuration where
# a keep that used to be implied quietly stops being one.
#
# Needs no secrets: the release build type assigns signingConfig only when jks.properties
# or KEY_ALIAS exists (`hasSigningCredentials` in app/build.gradle.kts, added so a fresh
# clone can still build), so this produces an unsigned release APK rather than dying in
# validateSigningFossRelease. Nothing here exports the keystore env vars, so the result is
# the same for a fork PR and for a branch PR.
#
# Both flavours, on every PR, deliberately. The two are not redundant: foss is the
# reproducibility-critical one and the only consumer of proguard-rules-foss.pro, while
# store is the only place the Play Billing dependency, its consumer keep rules and the
# app/src/store sources are shrunk at all — a store-only R8 break is invisible to a foss
# run. Gating store on `base_ref == 'master'` would defer exactly that check to the
# release PR, which is the one place a red check is most expensive. The marginal cost is
# small: lintStoreRelease above has already compiled the store release variant, so this
# adds shrink/dex/package for store rather than a second full Kotlin compile.
- name: Assemble minified release (R8)
id: r8
run: ./gradlew assembleFossRelease assembleStoreRelease --stacktrace
# R8 writes missing_rules.txt into the mapping directory when it cannot resolve a class
# something references, and the contents are the rules it wants. AGP fails the build in the
# same breath today, so this is a second lock rather than the only one — but it is the lock
# that survives the first being relaxed (a blanket -dontwarn added to quiet the symptom, or
# a future AGP demoting missing classes to a warning), and it names the file in the log
# instead of leaving it to be found inside an artifact.
- name: Check R8 missing rules
id: missing_rules
# Runs even when the assemble failed, because if R8 is what failed then this is the step
# that says so. Skipped along with the assemble if an earlier step went red.
if: ${{ !cancelled() && steps.r8.outcome != 'skipped' }}
run: |
missing=0
for rules in app/build/outputs/mapping/*/missing_rules.txt; do
[ -f "$rules" ] || continue
missing=1
echo "::error::R8 could not resolve every class the code references. The fix is on the classpath, not in the keep rules: add the dependency that supplies the class, or correct its scope (a compileOnly that should be implementation). A -keep cannot help here — it preserves a class R8 can already see, it does not supply one it cannot. $rules holds the -dontwarn rules R8 suggests so the build can proceed regardless; those silence the diagnostic rather than fix it, so add them to app/proguard-rules.pro (or the flavour's own file) only once you have established the class is genuinely absent at runtime too. If that code path does run, -dontwarn buys a ClassNotFoundException instead of a build failure."
cat "$rules"
done
exit "$missing"
# configuration.txt is the fully merged rule set, every consumer rule from every dependency
# included, and usage.txt is what R8 removed — between them a shrinker failure is readable
# from the PR without rerunning the build locally on a variant nobody assembles by hand.
# Failure-only, like the lint report above: on a green run the APK is unsigned and thrown
# away, and its mapping.txt describes nothing that will ever ship.
- name: Upload R8 outputs
if: failure() && steps.r8.outcome != 'skipped'
uses: actions/upload-artifact@v7
with:
# One directory per variant inside, so the archive names the variant even when the
# master-only store run above is part of it.
name: r8-outputs
path: app/build/outputs/mapping/
if-no-files-found: warn
retention-days: 14
# Runs on every PR rather than being path-filtered. on.pull_request.paths
# filters the whole workflow, which would also gate build-and-test — the
# required status check — leaving unrelated PRs pending forever.
shizu-manifest:
name: shizu-manifest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install check-jsonschema
run: pipx install check-jsonschema
- name: Check the Shizu store manifest
run: |
if [ ! -f shizu_store.json ]; then
echo "No shizu_store.json on this branch — nothing to check."
exit 0
fi
.github/scripts/check-shizu-manifest.sh