Releases built by OxygenFlow: the version in gradle.properties, and a release job that signs the APK with the app's key from the repository's secrets, checks it and keeps it with SHA256SUMS; 0.1.0 (refs #7) bfa2eff05d35
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key cfb5ece97d0d
- When
- · 2026-10-27 18:35 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
bfa2eff05d35b7dbb9b02d9c5d4c8ce681160955ef5713290cc8264b3aeeb767- Depends on
- 0a2858c0dad3 26530d9758ee 57c0824300db 8a657957d5a0 9481bfa7c5d0 e0e4a1c48a77 f821ee741b9f
- Depended on by
- 34a44dcebe06 8de06aa06268 e4b11612e7de 739d58b12359 1b7703ec4b02 1b55d7f47d9f 4a6d7ccd0958 797eca1a3e64 1823bc12eda1 bd1d3ad70c70 740a452b2669 23279b95b5b2 db7727e5de19 aa21eb5e0e5b e0004d61958c
- In bookmarks
- main
- In tags
- v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9
- Edits
- 10 atoms, 1 tree operation, 6 files
Browse the tree at this change
added scripts/release.sh +51 −0
| 1 | + | #!/usr/bin/env bash |
|
| 2 | + | # The app's release job (#7), in OxygenFlow's Android image with no network: |
|
| 3 | + | # a signed release APK of the version in gradle.properties, checked with |
|
| 4 | + | # apksigner, in dist/ beside its SHA256SUMS, for a release on oxygenhub.io. |
|
| 5 | + | # |
|
| 6 | + | # The key comes from the job's secrets, mounted as files and never in the |
|
| 7 | + | # environment: android-keystore (the PKCS12 keystore, base64) and |
|
| 8 | + | # android-keystore-password. Locally, give the same two files: |
|
| 9 | + | # KEYSTORE_B64=path PASSWORD_FILE=path scripts/release.sh |
|
| 10 | + | set -euo pipefail |
|
| 11 | + | cd "$(dirname "$0")/.." |
|
| 12 | + | b64=${KEYSTORE_B64:-/run/secrets/android-keystore} |
|
| 13 | + | password=${PASSWORD_FILE:-/run/secrets/android-keystore-password} |
|
| 14 | + | for f in "$b64" "$password"; do |
|
| 15 | + | [ -s "$f" ] || { echo "release: no $f: the job needs the android-keystore secrets" >&2; exit 1; } |
|
| 16 | + | done |
|
| 17 | + | version=$(sed -n 's/^oxygenhub\.version=//p' gradle.properties) |
|
| 18 | + | [ -n "$version" ] || { echo "release: no oxygenhub.version in gradle.properties" >&2; exit 1; } |
|
| 19 | + | ||
| 20 | + | # As ci.sh: the image's JDKs for Gradle's daemon, never a download. |
|
| 21 | + | jdks=$( (ls -d /opt/gradle-home/jdks/*/ 2>/dev/null || true) | sed 's:/$::' | paste -sd, -) |
|
| 22 | + | if [ -n "$jdks" ] && [ -n "${GRADLE_USER_HOME:-}" ]; then |
|
| 23 | + | mkdir -p "$GRADLE_USER_HOME" |
|
| 24 | + | { |
|
| 25 | + | echo "org.gradle.java.installations.paths=$jdks" |
|
| 26 | + | echo "org.gradle.java.installations.auto-download=false" |
|
| 27 | + | } >> "$GRADLE_USER_HOME/gradle.properties" |
|
| 28 | + | fi |
|
| 29 | + | ||
| 30 | + | # The keystore, decoded where only this job can read it, and gone after. |
|
| 31 | + | keys=$(mktemp -d) |
|
| 32 | + | trap 'rm -rf "$keys"' EXIT |
|
| 33 | + | chmod 700 "$keys" |
|
| 34 | + | base64 -d "$b64" > "$keys/release.p12" |
|
| 35 | + | export OXYGENHUB_KEYSTORE="$keys/release.p12" OXYGENHUB_KEYSTORE_PASSWORD_FILE="$password" |
|
| 36 | + | gradle=$(command -v gradle || echo ./gradlew) |
|
| 37 | + | "$gradle" --offline --no-daemon :app:assembleRelease |
|
| 38 | + | ||
| 39 | + | apk=app/build/outputs/apk/release/app-release.apk |
|
| 40 | + | [ -f "$apk" ] || { echo "release: no $apk (was it signed? an unsigned build is app-release-unsigned.apk)" >&2; exit 1; } |
|
| 41 | + | sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}} |
|
| 42 | + | apksigner=$(ls -d "$sdk"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1) |
|
| 43 | + | "$apksigner" verify --print-certs "$apk" | grep -E '^Signer #1 certificate (DN|SHA-256)' |
|
| 44 | + | ||
| 45 | + | # Kept as an artifact by a fixed name; SHA256SUMS gives the name it takes |
|
| 46 | + | # in a release, with the version (as OxygenHub's and OxygenFlow's dist do). |
|
| 47 | + | rm -rf dist |
|
| 48 | + | mkdir -p dist |
|
| 49 | + | cp "$apk" dist/oxygenhub-android-universal.apk |
|
| 50 | + | echo "$(sha256sum < dist/oxygenhub-android-universal.apk | cut -d' ' -f1) oxygenhub-android-$version-universal.apk" > dist/SHA256SUMS |
|
| 51 | + | echo "release: oxygenhub-android-$version-universal.apk" |
modified .flow/pipeline.kdl +32 −0
| … | 48 unchanged lines |
||
| 49 | 49 | } |
|
| 50 | 50 | } |
|
| 51 | 51 | } |
|
| 52 | + | ||
| 53 | + | // A signed release APK when the version in gradle.properties changes (#7), |
|
| 54 | + | // for a release on oxygenhub.io. The keystore and its password are the |
|
| 55 | + | // repository's secrets, mounted as files (never in the environment). |
|
| 56 | + | job "release" { |
|
| 57 | + | sandbox "android" |
|
| 58 | + | timeout "60m" |
|
| 59 | + | depends-on "test" |
|
| 60 | + | scope "gradle.properties" |
|
| 61 | + | mount { |
|
| 62 | + | source "ephemeral://disk" |
|
| 63 | + | target "/src/rust-core/target" |
|
| 64 | + | size "20GiB" |
|
| 65 | + | cache "rust-core-target" |
|
| 66 | + | key "rust-core/Cargo.lock" |
|
| 67 | + | } |
|
| 68 | + | secret "android-keystore" { |
|
| 69 | + | mount-path "/run/secrets/android-keystore" |
|
| 70 | + | } |
|
| 71 | + | secret "android-keystore-password" { |
|
| 72 | + | mount-path "/run/secrets/android-keystore-password" |
|
| 73 | + | } |
|
| 74 | + | exec "bash" "scripts/release.sh" |
|
| 75 | + | output { |
|
| 76 | + | artifact "oxygenhub-android-universal.apk" { |
|
| 77 | + | path "/src/dist/oxygenhub-android-universal.apk" |
|
| 78 | + | } |
|
| 79 | + | artifact "SHA256SUMS" { |
|
| 80 | + | path "/src/dist/SHA256SUMS" |
|
| 81 | + | } |
|
| 82 | + | } |
|
| 83 | + | } |
modified .oxyignore +1 −0
| … | 26 unchanged lines |
||
| 27 | 27 | # The CI job's reports (scripts/ci.sh). |
|
| 28 | 28 | /junit.xml |
|
| 29 | 29 | /coverage.xml |
|
| 30 | + | /dist/ |
modified CHANGELOG.md +4 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | ## 0.1.0 (2026-10-27) |
|
| 6 | + | ||
| 7 | + | - **Releases built by OxygenFlow (#7):** the app's version lives in `gradle.properties` (`oxygenhub.version`; the version code follows from it), and when it changes the pipeline's `release` job builds a release APK signed with the app's own key (a PKCS12 keystore and its password, the repository's `android-keystore` and `android-keystore-password` secrets, mounted as files), checks it with apksigner and keeps it with its `SHA256SUMS` for a release on oxygenhub.io. `scripts/release.sh` does the same locally with the key's files. The key's SHA-256 certificate fingerprint is `B0:99:57:36:44:42:AF:B2:27:28:B7:EF:93:F8:23:2D:F1:41:17:DA:50:28:6D:45:93:04:0F:9C:9F:A2:2A:48`; a release-signed app replaces a debug build only after uninstalling it. |
|
| 8 | + | ||
| 5 | 9 | - **The phone runner on OxygenFlow 0.5.0 (#22):** the app pins OxygenFlow 0.5.0 and oxy 0.8.0, so the phone's runner takes the idle claim that fits its memory (oxygenflow#92), stops a WASM job promptly (#93), offers the images it holds (#94) and reports where a job's time went (#35). Reinstall the app to move the runner from 0.3.1. |
|
| 6 | 10 | - **CI on OxygenFlow (#19):** every push builds the app and runs its unit tests in OxygenFlow's Android image, with no network, and keeps a JUnit report (`junit.xml`, the hub's Tests tab) and JaCoCo coverage (`coverage.xml`, the Coverage tab); `scripts/ci.sh` is the job. Debug builds record unit-test coverage (`./gradlew :app:createDebugUnitTestCoverageReport`). The screenshot tests stay local: their references change with the clock, so are not kept. |
|
| 7 | 11 | - **Account and organisation pages:** an owner's page shows who they are (avatar, name, when they joined; an organisation's description and members, each opening their own page), their open project boards and the repositories of theirs you can read, newest first. Opened from a repository's top bar, from a member, and from hub links to `/{owner}` or `/orgs/{name}`. From `GET /users/{name}`, else `GET /orgs/{name}`. |
|
| … | 10 unchanged lines |
modified app/build.gradle.kts +26 −2
| … | 50 unchanged lines |
||
| 51 | 51 | ||
| 52 | 52 | tasks.named("preBuild") { dependsOn(generateRustBindings) } |
|
| 53 | 53 | ||
| 54 | + | // The version, from gradle.properties (#7). |
|
| 55 | + | val appVersion: String = providers.gradleProperty("oxygenhub.version").get() |
|
| 56 | + | val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c } |
|
| 57 | + | ||
| 58 | + | // Release signing (#7): the release job decodes its keystore secret to a |
|
| 59 | + | // file and names it and the password's file here. Without them a release |
|
| 60 | + | // build is unsigned, so nothing else needs the key. |
|
| 61 | + | val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile } |
|
| 62 | + | val releasePassword: String? = |
|
| 63 | + | providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() } |
|
| 64 | + | ||
| 54 | 65 | android { |
|
| 55 | 66 | namespace = "io.oxygenhub.android" |
|
| 56 | 67 | compileSdk { |
|
| … | 4 unchanged lines |
||
| 61 | 72 | applicationId = "io.oxygenhub.android" |
|
| 62 | 73 | minSdk = 31 |
|
| 63 | 74 | targetSdk = 37 |
|
| 64 | - | versionCode = 1 |
|
| 65 | - | versionName = "1.0" |
|
| 75 | + | versionCode = appVersionCode |
|
| 76 | + | versionName = appVersion |
|
| 66 | 77 | ||
| 67 | 78 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | signingConfigs { |
|
| 82 | + | if (releaseKeystore != null && releasePassword != null) { |
|
| 83 | + | create("release") { |
|
| 84 | + | storeFile = releaseKeystore |
|
| 85 | + | storeType = "PKCS12" |
|
| 86 | + | storePassword = releasePassword |
|
| 87 | + | keyAlias = "oxygenhub" |
|
| 88 | + | keyPassword = releasePassword |
|
| 89 | + | } |
|
| 90 | + | } |
|
| 68 | 91 | } |
|
| 69 | 92 | ||
| 70 | 93 | buildTypes { |
|
| … | 6 unchanged lines |
||
| 77 | 100 | optimization { |
|
| 78 | 101 | enable = false |
|
| 79 | 102 | } |
|
| 103 | + | signingConfigs.findByName("release")?.let { signingConfig = it } |
|
| 80 | 104 | } |
|
| 81 | 105 | } |
|
| 82 | 106 | compileOptions { |
|
| … | 54 unchanged lines |
modified gradle.properties +3 −0
| … | 18 unchanged lines |
||
| 19 | 19 | kotlin.code.style=official |
|
| 20 | 20 | # Compose Preview Screenshot Testing (the README's screenshots). |
|
| 21 | 21 | android.experimental.enableScreenshotTest=true |
|
| 22 | + | # The app's version (#7): the release job runs when this changes, and the |
|
| 23 | + | # version code follows from it (major * 10000 + minor * 100 + patch). |
|
| 24 | + | oxygenhub.version=0.1.0 |