A release-check job: the release's secrets checked in seconds, with no build (refs #7) 23279b95b5b2
scripts/release-check.sh prints the keystore's SHA-256 and the entry its password opens, checks the key oxygenhub opens with that password as signing needs, and that Firebase's settings are whole JSON; never a key, a password or the settings. A manual job runs it (oh run start --job release-check), and release.sh runs it before its build. 0.1.0 takes everything on main.
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key dd0d13fb53b4
- When
- · 2026-10-28 11:01 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
23279b95b5b206c680a9ed39b13ebf63aef91df0f1b57581a5dd5dc3d1b5d3c4- Depends on
- 57c0824300db 6e2af63ae616 9481bfa7c5d0 aa21eb5e0e5b bfa2eff05d35 e0004d61958c f821ee741b9f
- Depended on by
- 840c75813f86 740a452b2669 a8e798f6ccae
- 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
- 6 atoms, 1 tree operation, 4 files
Browse the tree at this change
added scripts/release-check.sh +55 −0
| 1 | + | #!/usr/bin/env bash |
|
| 2 | + | # The release's secrets checked without a build (#7), by the release-check |
|
| 3 | + | # job in seconds and by scripts/release.sh before its own: the keystore's |
|
| 4 | + | # SHA-256 and the entry its password opens (compare with the keystore you |
|
| 5 | + | # hold: `sha256sum FILE`, `keytool -list`), and that Firebase's settings |
|
| 6 | + | # are whole JSON. Never prints a key, a password or the settings. |
|
| 7 | + | # KEYSTORE_B64=path PASSWORD_FILE=path [FIREBASE_CONFIG=path] scripts/release-check.sh |
|
| 8 | + | set -euo pipefail |
|
| 9 | + | b64=${KEYSTORE_B64:-/run/secrets/android-keystore} |
|
| 10 | + | password=${PASSWORD_FILE:-/run/secrets/android-keystore-password} |
|
| 11 | + | firebase=${FIREBASE_CONFIG:-/run/secrets/firebase-config} |
|
| 12 | + | for f in "$b64" "$password"; do |
|
| 13 | + | [ -s "$f" ] || { echo "release-check: no $f: the job needs the android-keystore secrets" >&2; exit 1; } |
|
| 14 | + | done |
|
| 15 | + | ||
| 16 | + | keys=$(mktemp -d) |
|
| 17 | + | trap 'rm -rf "$keys"' EXIT |
|
| 18 | + | chmod 700 "$keys" |
|
| 19 | + | base64 -d "$b64" > "$keys/release.p12" || { echo "release-check: the android-keystore secret is not base64 (base64 -w0 FILE.p12)" >&2; exit 1; } |
|
| 20 | + | echo "release-check: keystore sha256 $(sha256sum < "$keys/release.p12" | cut -d' ' -f1)" |
|
| 21 | + | keytool=$(command -v keytool || (ls /opt/gradle-home/jdks/*/bin/keytool 2>/dev/null || true) | head -1) |
|
| 22 | + | if [ -z "$keytool" ]; then |
|
| 23 | + | echo "release-check: no keytool here, so the password is not checked" >&2 |
|
| 24 | + | else |
|
| 25 | + | # As the build reads it: the whole file, trimmed. |
|
| 26 | + | tr -d '\r' < "$password" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | head -c 4096 > "$keys/pass" |
|
| 27 | + | "$keytool" -list -keystore "$keys/release.p12" -storetype PKCS12 -storepass:file "$keys/pass" 2>&1 \ |
|
| 28 | + | | grep -E 'PrivateKeyEntry|SHA-256\)' \ |
|
| 29 | + | || { echo "release-check: the android-keystore-password secret does not open the android-keystore secret, or it holds no key" >&2; exit 1; } |
|
| 30 | + | "$keytool" -list -keystore "$keys/release.p12" -storetype PKCS12 -storepass:file "$keys/pass" 2>/dev/null | grep -q '^oxygenhub,' \ |
|
| 31 | + | || { echo "release-check: the keystore has no entry named oxygenhub, which the build signs with" >&2; exit 1; } |
|
| 32 | + | # Signing reads the private key with the same password: a request for a |
|
| 33 | + | # certificate uses the key, so it fails just as signing would. |
|
| 34 | + | "$keytool" -certreq -alias oxygenhub -keystore "$keys/release.p12" -storetype PKCS12 \ |
|
| 35 | + | -storepass:file "$keys/pass" -keypass:file "$keys/pass" > /dev/null 2> "$keys/certreq.err" \ |
|
| 36 | + | || { echo "release-check: the key oxygenhub cannot be read with the keystore's password, as signing needs: $(head -c 300 "$keys/certreq.err")" >&2; exit 1; } |
|
| 37 | + | echo "release-check: the key oxygenhub opens with the password" |
|
| 38 | + | if [ "$(wc -l < "$password")" -gt 1 ]; then |
|
| 39 | + | echo "release-check: the password secret has more than one line; the build reads all of it" >&2 |
|
| 40 | + | fi |
|
| 41 | + | fi |
|
| 42 | + | ||
| 43 | + | if [ -s "$firebase" ]; then |
|
| 44 | + | if command -v python3 > /dev/null; then |
|
| 45 | + | python3 -I -c 'import json,sys; json.load(open(sys.argv[1]))' "$firebase" 2>/dev/null \ |
|
| 46 | + | || { echo "release-check: the firebase-config secret is not whole JSON (is its last } missing?)" >&2; exit 1; } |
|
| 47 | + | elif [ "$(tr -d '[:space:]' < "$firebase" | tail -c 1)" != "}" ]; then |
|
| 48 | + | echo "release-check: the firebase-config secret does not end with } (is its last line missing?)" >&2 |
|
| 49 | + | exit 1 |
|
| 50 | + | fi |
|
| 51 | + | echo "release-check: firebase-config reads as JSON" |
|
| 52 | + | else |
|
| 53 | + | echo "release-check: no $firebase: a release takes no alerts" >&2 |
|
| 54 | + | fi |
|
| 55 | + | echo "release-check: ok" |
modified .flow/pipeline.kdl +21 −0
| … | 49 unchanged lines |
||
| 50 | 50 | } |
|
| 51 | 51 | } |
|
| 52 | 52 | ||
| 53 | + | // The release's secrets checked in seconds, with no build (#7): the |
|
| 54 | + | // keystore's SHA-256, the key its password opens and Firebase's JSON. |
|
| 55 | + | // By hand only: oh run start --job release-check. |
|
| 56 | + | job "release-check" { |
|
| 57 | + | sandbox "android" |
|
| 58 | + | timeout "5m" |
|
| 59 | + | on { |
|
| 60 | + | manual |
|
| 61 | + | } |
|
| 62 | + | secret "android-keystore" { |
|
| 63 | + | mount-path "/run/secrets/android-keystore" |
|
| 64 | + | } |
|
| 65 | + | secret "android-keystore-password" { |
|
| 66 | + | mount-path "/run/secrets/android-keystore-password" |
|
| 67 | + | } |
|
| 68 | + | secret "firebase-config" { |
|
| 69 | + | mount-path "/run/secrets/firebase-config" |
|
| 70 | + | } |
|
| 71 | + | exec "bash" "scripts/release-check.sh" |
|
| 72 | + | } |
|
| 73 | + | ||
| 53 | 74 | // A signed release APK when the version in gradle.properties changes (#7), |
|
| 54 | 75 | // for a release on oxygenhub.io. The keystore and its password are the |
|
| 55 | 76 | // repository's secrets, mounted as files (never in the environment). |
|
| … | 32 unchanged lines |
modified CHANGELOG.md +2 −3
| 1 | 1 | # Changelog |
|
| 2 | 2 | ||
| 3 | - | ## Unreleased |
|
| 3 | + | ## 0.1.0 (2026-10-28) |
|
| 4 | 4 | ||
| 5 | + | - **The release's secrets checked in seconds (#7):** `oh run start --job release-check` runs `scripts/release-check.sh` with no build: the keystore's SHA-256 and the entry its password opens (to compare with the keystore you hold), that the key `oxygenhub` opens with that password as signing needs, and that Firebase's settings are whole JSON; never a key, a password or the settings. The release job runs the same check first. |
|
| 5 | 6 | - **The hub's living backgrounds (#23):** the website's backdrops come to the phone, the same Rust scenes (copied from OxygenHub's crates/backdrop by scripts/sync-backdrop.sh) stepped in rust-core and drawn by the app. Sign-in has the OxygenHub mark, which answers a finger; the dashboard has Your sky, your repositories and their last 30 days of pushes, proposals, issues, releases and runs, each mark a real event (OxygenHub's `GET /user/sky`; a hub without it shows the quiet field), drawing near your finger and naming the repository it rests by, behind cards it shows through. Live runs and jobs pulse; empty and error screens have the quiet field the hub's error pages have; a screen opened slides in a little from the side it leads to, and goes back the way it came. A repository's screen opens on a band of its own sky, its last 30 days as its pages on the hub draw them (OxygenHub's `GET /repos/OWNER/NAME/sky`; shown only when the hub gives it). As on the web, drawing slows 3 s after the last touch and stops after 2 minutes, and it holds still with the system's animations off or battery saver on. |
|
| 6 | 7 | - **Assign issues from the phone (#4):** an issue's Assignees action shows who it is assigned to, each removable, with Assign me and a field for anyone's username; saved as the whole set (triage access on the hub). |
|
| 7 | 8 | - **Approve, reject and expedite runs (#5):** a run's job that waits for a person shows Approve and Reject (its approval rules on the hub decide who may), and a live run has Expedite, which asks why it is urgent (a maintainer's, within the owner's cap). Needs OxygenHub 0.9.11 (the job's `approval` and the expedite route). |
|
| … | 1 unchanged lines |
||
| 9 | 10 | - **A proposal's files and their diffs (#4):** a proposal's Changes section opens Files changed: each file it changes, folding open to its diff (old and new line numbers, added and removed lines tinted), as its page on the hub shows them, from the hub's `GET …/proposals/N/diff` (OxygenHub 0.9.10). The change screen and this one share the same diff list. |
|
| 10 | 11 | - **Clone a repository from the phone (#3):** the repository screen's Clone action shows its `oxy clone` command, selectable, with a button that copies it. |
|
| 11 | 12 | ||
| 12 | - | ## 0.1.0 (2026-10-27) |
|
| 13 | - | ||
| 14 | 13 | - **Alerts from the hub (#16):** what your inbox on the hub is told (builds broken and fixed, reviews, mentions…) also reaches the phone through Firebase Cloud Messaging, sealed to an X25519 key the phone made (kept wrapped by the Android Keystore), so Google carries only ciphertext; the app opens it with the Rust core (`flow_core::alert`, as the hub seals it) and shows it, and a tap opens its link. Each signed-in session registers the phone (`PUT /user/push`); signing out ends it. Firebase starts by hand from `google-services.json` (the release job's `firebase-config` secret), with no Google Gradle plugin and only `firebase-messaging`; a build without it takes no alerts. The hub sends once it runs on GCP. |
|
| 15 | 14 | - **The app says when it is out of date (#7):** Account shows the app's version and, signed in to oxygenhub.io, whether a later full release of andymccall/oxygenhub-android is out, with its APK to download; opening the file updates the app, since every release is signed with the same key. Pre-releases are not offered. |
|
| 16 | 15 | - **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. |
|
| … | 14 unchanged lines |
modified scripts/release.sh +2 −0
| … | 17 unchanged lines |
||
| 18 | 18 | for f in "$b64" "$password"; do |
|
| 19 | 19 | [ -s "$f" ] || { echo "release: no $f: the job needs the android-keystore secrets" >&2; exit 1; } |
|
| 20 | 20 | done |
|
| 21 | + | # The secrets first, in seconds, before a build that takes a quarter of an hour. |
|
| 22 | + | KEYSTORE_B64="$b64" PASSWORD_FILE="$password" FIREBASE_CONFIG="$firebase" bash scripts/release-check.sh |
|
| 21 | 23 | version=$(sed -n 's/^oxygenhub\.version=//p' gradle.properties) |
|
| 22 | 24 | [ -n "$version" ] || { echo "release: no oxygenhub.version in gradle.properties" >&2; exit 1; } |
|
| 23 | 25 | ||
| … | 38 unchanged lines |