docs(billing): name the real reconnection backstop, not the one the library does not provide #304
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |