Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/scripts/check-app-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ cd "$(git rev-parse --show-toplevel)"
version=$(jq -r '.version' app/version.json)
code=$(jq -r '.versionCode' app/version.json)

if ! jq -e '(.version | type == "string") and (.versionCode | type == "number")' app/version.json > /dev/null; then
echo "::error file=app/version.json::version must be a JSON string and versionCode a JSON number, because latest.json copies them as they are."
exit 1
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error file=app/version.json::version must look like 1.2.3, got '$version'."
exit 1
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/app-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ name: app-release
# its app-v<version> tag exists, so the next run tries again.
#
# The APK is always attached as life-app.apk, because the app's "Update
# required" screen links to releases/latest/download/life-app.apk.
# required" screen links to releases/latest/download/life-app.apk. It is
# attached with latest.json ({version, versionCode}), which the app fetches from
# releases/latest/download/latest.json to find out whether a newer one exists.

on:
push:
Expand Down Expand Up @@ -105,11 +107,15 @@ jobs:
echo "Signed with the release key ($actual)"
env:
EXPECTED_CERT_SHA256: ${{ vars.ANDROID_RELEASE_CERT_SHA256 }}
# Both files go in one command: gh uploads them to a draft and publishes it
# (creating the tag) only when all are there, so latest.json never appears
# without the APK, or before the signing check above has passed.
- name: Publish the release
run: |
version=$(jq -r '.version' app/version.json)
cp "$APK" life-app.apk
gh release create "app-v$version" life-app.apk \
jq '{version, versionCode}' app/version.json > latest.json
gh release create "app-v$version" life-app.apk latest.json \
--target "$GITHUB_SHA" \
--title "Life App $version" \
--generate-notes
Expand Down
4 changes: 2 additions & 2 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The dev variant calls the API at `http://10.0.2.2:8080`, the emulator's address

## Releasing

A release is a GitHub release with the signed production APK attached as `life-app.apk`. The phones install it directly. [`releases/latest/download/life-app.apk`](https://github.com/lakaniemi/life-app/releases/latest/download/life-app.apk) always points at the newest one, and the "Update required" screen links there.
A release is a GitHub release with the signed production APK attached as `life-app.apk`. The phones install it directly. [`releases/latest/download/life-app.apk`](https://github.com/lakaniemi/life-app/releases/latest/download/life-app.apk) always points at the newest one, and the "Update required" screen links there. The release also carries `latest.json`, a copy of `version.json` (`{ "version", "versionCode" }`) that the app fetches from [`releases/latest/download/latest.json`](https://github.com/lakaniemi/life-app/releases/latest/download/latest.json) to see whether a newer version exists.

To release, open a PR that bumps both fields in `version.json`, and merge it:

Expand All @@ -52,7 +52,7 @@ To release, open a PR that bumps both fields in `version.json`, and merge it:
- `version` is the label people see, as `X.Y.Z`. It must be higher than the last release's.
- `versionCode` is the integer Android compares, and it refuses to install a lower one over a higher one. It must go up by at least one with every release.

The app's CI fails the PR when the bump is inconsistent. After the merge, the `app-release` workflow builds the APK, checks that it's signed with the release key, and publishes the `app-v<version>` release with notes listing the merged PRs. A merge that doesn't change `version.json` releases nothing. If a run fails, fix the cause and re-run the workflow from the Actions tab: a release only exists once its tag does.
The app's CI fails the PR when the bump is inconsistent. After the merge, the `app-release` workflow builds the APK, checks that it's signed with the release key, and publishes the `app-v<version>` release with notes listing the merged PRs, uploading `life-app.apk` and `latest.json` together: the release is published (and its tag created) only once both are there, so `latest.json` never announces an APK that isn't. A merge that doesn't change `version.json` releases nothing. If a run fails, fix the cause and re-run the workflow from the Actions tab: a release only exists once its tag does. A run that dies during the upload can leave an unpublished draft release behind, which is safe to delete by hand.

### Signing key (one-time setup)

Expand Down
2 changes: 1 addition & 1 deletion specs/app-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Every screen uses the same states:
- Technical design: [`app/docs/ARCHITECTURE.md`](../app/docs/ARCHITECTURE.md).
- API: the minimum compatibility level is enforced by `api/internal/server/compat.go`.
- App: most of this is built (the App baseline slice), and app updates are left for the App updates slice.
- Releases: `.github/workflows/app-release.yml` publishes the signed APK as `life-app.apk` on a GitHub release whenever `app/version.json` is bumped on `main`. `.github/scripts/check-app-version.sh` checks the bump on pull requests. The signing is `app/plugins/with-release-signing.ts`, and how to release is in [`app/README.md`](../app/README.md#releasing).
- Releases: `.github/workflows/app-release.yml` publishes the signed APK as `life-app.apk` on a GitHub release whenever `app/version.json` is bumped on `main`, together with `latest.json` (`version` and `versionCode`), which the app reads to find a newer release. `.github/scripts/check-app-version.sh` checks the bump on pull requests. The signing is `app/plugins/with-release-signing.ts`, and how to release is in [`app/README.md`](../app/README.md#releasing).
- The shared screen states are in `app/src/ui/` (`loading-state.tsx`, `empty-state.tsx`, `error-state.tsx`), and the snackbar host is `app/src/ui/snackbar.tsx` (#23).
- The compatibility level, the header and the "outdated" flag are in `app/src/api/compat.ts` and `client.ts`. The "Update required" screen and the gate that shows it are in `app/src/features/app-update/`. It links to the APK only, until the App updates slice adds Install.
- The API client, error copy and query setup are in `app/src/api/`: the persisted cache and "Couldn't refresh" in `query-client.ts` and `query-provider.tsx`, refetching on focus and reconnect in `managers.ts` and `use-api-query.ts`, and the offline first load in `query-state.ts`. The offline banner is `app/src/ui/offline-banner.tsx`, shown by `Screen` (#24).
Loading