Skip to content

Latest commit

 

History

History
262 lines (212 loc) · 16 KB

RELEASE.md

File metadata and controls

262 lines (212 loc) · 16 KB

Releasing

How a version of AVR-Remote gets from master to users. Written down after the 1.6.0 release, because none of it was recorded anywhere and most of it is not guessable from the repository.

What the automation does — and what it does not

.github/workflows/releasebuild.yml triggers on a tag matching v*. It builds with JDK 17, signs the release APK from the repository secrets, renames it to avr-remote-<tag>.apk and attaches it to a GitHub Release, whose description it fills from the release notes (see below). A tag containing rc produces a pre-release.

The description is generated from app/src/main/assets/whatsnew.html, the same file the in-app dialog renders: misc/release-notes.py cuts out the block whose <b>version</b> heading matches the tag without its leading v and turns the <li> items into Markdown bullets. So the notes are written once, in step 2 below, and both places get them.

This is a deliberate trade, and it costs. The workflow used to set no description at all, so the 1.6.0 and 1.6.1 pages were written by hand afterwards — and written longer than the in-app dialog: around 3400 and 2400 characters, with an opening paragraph, a warning quote, ### Fixed / ### Changed headings and the reasoning behind each fix. The generated bullets for 1.6.1 are 863 characters and flat. What was bought is that nobody has to remember the second step; what was given up is that depth. If a release deserves the long form, write it into the release page by hand after the workflow has run — but then it lives only there, which is the state this replaced.

The conversion is lossy in one more way worth knowing: a link inside an entry keeps its text and loses its href, so entries have to read as prose rather than as "see here".

A missing block costs the description, never the release. The script then prints nothing, exits 1, and the workflow falls back to GitHub's generated list of merged pull requests — which is a diff summary, not a user-facing one. That is the intended behaviour for an rc tag, and a warning sign for anything else: by the time the step runs the tag is pushed and the APK is built, so failing there would leave a tag with no release at all. The two things that trigger it are notes that were never written and a heading that does not match the tag exactly — <b>v1.6.1</b> or <b>1.6.1a</b> for tag v1.6.1 do not match. Only whitespace inside the <b> is tolerated.

There is no Play Store automation. No fastlane, no gradle-play-publisher, no service account — the Play upload has always been done by hand in the Play Console, and still has to be. The GitHub Release is where the APK comes from; the Console is where it goes.

Signing

The signingConfigs block in app/build.gradle is conditional. It is only created when either ~/keystore.properties is readable or KEY_PASSWORD is set in the environment. With neither, the block is skipped entirely and the build produces app-release-unsigned.apk — silently, on a local build. If a release APK cannot be installed, this is why. In CI the same situation is loud rather than silent, but for an unhelpful reason: the Rename APK step moves app-release.apk, which does not exist, and the job fails there instead of at signing.

Source Where
CI Repository secrets KEY_JKS (base64 of the JKS), KEY_PASSWORD, KEY_ALIAS, STORE_PASSWORD. The workflow decodes KEY_JKS to key.jks and hardcodes STORE_FILE=../key.jks, relative to the app module.
Local ~/keystore.properties with keyAlias / keyPassword / storeFile / storePassword, or the same four values as KEY_ALIAS / KEY_PASSWORD / STORE_FILE / STORE_PASSWORD environment variables.

The keystore itself is not in the repository and must never be. misc/genkey.sh is the script it was presumably created with, and its alias is pskiwi — but that is the script's default, not proof: the alias CI actually signs with comes from the KEY_ALIAS secret and cannot be read from here. apksigner verify --print-certs on a released APK is the way to find out for real.

The keystore is a single point of failure. Verify this in the Play Console rather than trusting it: as of the 1.6.0 release the app is not enrolled in Play App Signing, which means Google holds no copy. Lose the keystore and the app can never be updated again — only republished under a new package name, losing every install, rating and review. Keep an offline backup, or enrol in Play App Signing (uploading the existing key keeps everything else unchanged).

Steps

  1. Raise the version. versionCode and versionName live in app/src/main/AndroidManifest.xml and nowhere else — there is no versionCode in app/build.gradle. Both must be raised; Play rejects a versionCode that is not higher than the last one.

  2. Add release notes in two places, which say the same thing at different lengths.

    app/src/main/assets/whatsnew.html is the in-app dialog and the GitHub release description — a new <b>version</b> block at the top of the list, with the version written exactly as in the tag but without the v. AboutActivity renders the file, and AVRRemote opens it automatically on the first start after an update, but only once a receiver is configured: the check in AVRRemote.showChangeLog() is getConnectionConfig().isDefined() && AVRSettings.isShowChangeLog(this), and it waits until the permission dialogs of that start are answered. isShowChangeLog() compares packageInfo.versionCode against the stored AVRLastVersionCode preference, so an unchanged versionCode means nobody ever sees the notes.

    English only. The app ships two locales, but this file is not one of the localised resources — there is a single assets/whatsnew.html, and German readers get the English text in the dialog just as they do on the release page. Adding a German block would show it to everyone, twice. The German wording belongs in version.xml below, where the Console splits it by language tag.

    version.xml is the Play Store wording, 500 characters per language. It is not checked in; .gitignore covers it, because it is input for the Console rather than part of the app. Its shape:

    <de-DE>
    …
    </de-DE>
    
    <en-US>
    …
    </en-US>

    Deliberately not a well-formed XML document — two roots, no declaration — because that is what the Console expects when it splits the text by language tag. The app ships exactly these two locales (res/values and res/values-de).

    Credit every entry with the GitHub login of whoever made the change, in trailing parentheses: (pskiwi), or (#13/netmindz) when there is a pull request or issue to point at. Get the names from git log --format='%h %an <%ae> %s' <last-tag>..HEAD --no-merges and map the addresses to logins with gh pr view <n> --json author. Do not take the login from the merge commit subject: PR #9 reads "Merge pull request #9 from EMATech/master", but EMATech is the organisation that owns the fork — the author is rdoursenaud.

    Write for users, not for the diff. Two kinds of entry are easy to leave out and both matter more than any feature:

    • A raised minSdk strands devices. 1.6.0 went from minSdk 8 to 24, so everything below Android 7.0 stays on 1.5.1 forever. Say so in the first bullet.
    • Rewrites of the receiver-facing code, even when nothing is meant to change for the user. The 1.6.0 replacement of Apache HttpClient could only be tested against the hardware that was available; a user on an untested model needs to know that a missing input name is worth reporting.

    Last, read back what the release page will show — before tagging, not after:

    python3 misc/release-notes.py v1.6.0
  3. Remove the items in TODO.md that this release closes — that file records open work only, the history records what was done. If @array/modelNames gained entries since the last release, append them to RELEASED in ReleasedModelNamesTest: from now on they are stored in users' settings, and a class that disappears would drop those users to AVRGeneric silently.

  4. Commit, tag, push. The tag convention is v<versionName>:

    git tag v1.6.0
    git push origin master --tags
  5. Watch the build, then fetch the APK and verify the signature before handing it to anyone. Read the release description in the same pass: a list of merged pull requests instead of the notes means the block was not found and the fallback took over (see above).

    gh run watch
    gh release view v1.6.0
    gh release download v1.6.0
    $ANDROID_HOME/build-tools/<version>/apksigner verify --print-certs avr-remote-v1.6.0.apk
  6. Upload to the Play Console by hand — see below.

Play Console

Every release

  • Upload the APK from the GitHub Release to a production release.

  • Paste the release notes from the local version.xml (see step 2) — the <de-DE> and <en-US> blocks, without the comment. Play truncates silently at 500 characters per language, so count first, stripping the comment or the tag names quoted inside it throw the match off:

    ```sh
    python3 -c "import re; s=re.sub(r'<!--.*?-->','',open('version.xml').read(),flags=re.S); \
      [print(t, len(b.strip())) for t,b in re.findall(r'<(\S+?)>(.*?)</\1>', s, re.S)]"
    ```
    
  • Check that no declaration on the App content page has gone red since last time. An outstanding declaration blocks all changes, including store-listing edits.

  • If minSdk was raised, note that the device catalogue shrinks accordingly and the affected users silently stop receiving updates.

  • Still outstanding from 1.6.0: replace the store-listing icon with misc/play-store-icon-512.png. The listing still shows the raster icon from 2010; the adaptive icon shipped in the APK is not used for the listing. Regenerate the PNG from misc/play-store-icon.svg if the artwork changes — the command is a comment at the top of that file, and the artwork has four copies in the tree that have to move together (app/src/main/res/drawable/ic_launcher_foreground.xml, app/src/main/assets/icon.svg, docs/avr-icon.svg and the store SVG).

Store listing

Not part of a release — the listing has its own review and can be edited at any time. The text lives in store-listing.txt, both languages, short and full description, with the character limits and a command to count them. Unlike version.xml it is checked in, because it stays in the store for years and the next edit should start from it.

Google rejected the listing in August 2026 with "Die Funktionen Ihrer App werden in Ihrem Store-Eintrag nicht deutlich beschrieben", and was right: the old text was two sentences, a model list and disclaimers, and never said what the app does. The rewrite puts the features first and the model list last. Keep that order.

The model list there comes from @array/modelNames in res/values/lists.xml, not from the previous listing — that one had missed seven models and dropped the dashes from five. That makes four copies of the model list in the tree: lists.xml, README.md, docs/index.md and store-listing.txt. Only the first is pinned by a test (ModelConfiguratorTest, against the classes in models/); the other three drift silently, and two of them had. Regenerate them from lists.xml rather than editing by hand:

python3 -c "import re; s=open('app/src/main/res/values/lists.xml').read(); \
  m=re.search(r'<string-array name=\"modelNames\".*?</string-array>', s, re.S); \
  print(', '.join(i.strip() for i in re.findall(r'<item>(.*?)</item>', m.group(0), re.S)))"

Recommendations to decline

The Console attaches "recommended actions" to every release. They are generic, they do not know what the app is, and they come back each time. Decided once, so they do not have to be re-argued:

  • "Enable R8 to improve memory and performance" — no. ModelConfigurator.java:84 resolves the 60 receiver classes by name, Class.forName("de.pskiwi.avrremote.models." + avrModel). They have no static references, so shrinking removes all of them and every user falls back to AVRGeneric.

    What makes that bad is not that features disappear — it is that they do not. AVRGeneric extends AbstractModel, which answers true to hasZones(), hasQuick() and hasLevels() and reports TYPE_7CH levels, and AVRGeneric.getZoneCount() returns 3. So every receiver would be driven as a generic 3-zone, 7-channel device with quick-select: a phantom zone on 2-zone models, a missing one on 4-zone models, the generic input and surround lists instead of the model's own, and quick-select offered where the hardware has none. Only supportsDAB() is an outright loss. The app would look like it works and send the wrong commands.

    ModelConfiguratorTest does not protect against this: it runs on the JVM against unminified classes, so the build stays green and the damage only appears in the field. Turning R8 on would need -keep class de.pskiwi.avrremote.models.** { *; } and verification on a device with a real model selected — for close to nothing, because the app has no runtime dependencies and the APK is 343 KB. Shrinking libraries is where R8 normally earns its keep, and there are none. See also CLAUDE.md → Receiver models.

Deadlines

Everything below was checked in August 2026 and none of it is under our control, so re-read the sources rather than this list — Play policy moves and this file will not.

  • Target API level. Play requires API 35 today and API 36 from 31 August 2026 for new apps and updates. The app is at targetSdk 37, so it clears both with a year to spare and has already taken the step that matters: API 37 brings mandatory Local Network Protection, which gates the subnet scan and every receiver socket — see TODO.md → Android 17: Local Network Protection and CONNECTION.md. The Play deadline for API 37 is expected around August 2027; Google has not announced it yet. 1.7.0 (tagged 2026-09-27) is the first release with targetSdk 37 and is out. (requirements, local network permission)
  • Package name registration — 30 September 2026. Part of the Android developer verification programme. Google auto-registered nearly all existing Play apps in March 2026; confirm on the Play Console home page that de.pskiwi.avrremote is listed as registered. Unregistered apps face removal, starting regionally in late 2026. (details)
  • Data safety form. Mandatory, must stay accurate. The app collects nothing and requests only INTERNET, ACCESS_NETWORK_STATE, ACCESS_LOCAL_NETWORK and POST_NOTIFICATIONS (ACCESS_WIFI_STATE went in 1.7.0). (details)
  • Content rating (IARC). Since the policy update of 15 July 2026 unrated apps are not allowed on Play. Fill in the questionnaire if it has lapsed. (announcement)

APK or App Bundle?

Apps that were on Play before August 2021 may keep shipping APKs; the App Bundle requirement applies to apps published since then (and, since June 2023, to Android TV updates). AVR-Remote predates the cutoff, which is why the workflow still produces an APK.

If the Console ever refuses the APK, switching is not a one-line change: an AAB requires enrolling in Play App Signing, which is irreversible. Add bundleRelease to the workflow, publish app/build/outputs/bundle/release/app-release.aab, and upload the existing keystore to Google so that existing installs keep updating.