Bladeren bron

feat: publish Gogs releases through the web form

Gogs only exposes GET for releases, so add a helper that logs in with local web credentials, submits the release form, and verifies publication. Store web credentials in a gitignored mode-600 file so they survive environment rebuilds. Publish 1.0.7 release notes.
Krasimir Kazakov 6 dagen geleden
bovenliggende
commit
5a63ef9553
5 gewijzigde bestanden met toevoegingen van 211 en 9 verwijderingen
  1. 3 0
      .gitignore
  2. 7 5
      AGENTS.md
  3. 4 4
      README.md
  4. 27 0
      releases/1.0.7.md
  5. 170 0
      tool/gogs_release.sh

+ 3 - 0
.gitignore

@@ -49,3 +49,6 @@ app.*.map.json
 
 # Local Gogs release token; never commit.
 android/keystore/gogs_token
+
+# Local Gogs web credentials; never commit.
+android/keystore/gogs_credentials

+ 7 - 5
AGENTS.md

@@ -122,7 +122,9 @@ Release checklist:
 3. Build the universal APK:
    `JAVA_HOME=/usr/lib/jvm/java-21-openjdk flutter build apk --release --target-platform android-arm,android-arm64,android-x64`
 4. Copy to `releases/cardash-android-<version>.apk`, write `releases/cardash-android-<version>.apk.sha256`, update `releases/README.md`, then commit and push.
-5. Create a Gogs release at `/releases/new`: tag `v<version>`, target `main`, title/body, link the raw APK:
+5. Create and publish a Gogs release:
+   - Preferred: `./tool/gogs_release.sh v<version> "CarDash <version>" releases/<version>.md`. It uses the web form (the API cannot create releases), reads credentials locally, and never prints them.
+   - Or use `/releases/new` in the web UI: tag `v<version>`, target `main`, title/body, link the raw APK:
    `https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-<version>.apk`.
    Gogs attachments cap at 32 MB, so the ~54 MB universal APK is linked from the release body instead of attached.
 6. Publishing the Gogs release is the deployment for in-app updates. The app queries
@@ -134,8 +136,8 @@ Release checklist:
 
 ## 8. Gogs credentials
 
-- Never commit, print, or paste the Gogs token into chat, commit messages, logs, release bodies, or shell history. It is not part of the repository.
-- Read it from the `GOGS_TOKEN` environment variable, else `$HOME/.config/cardash/gogs_token`, else `android/keystore/gogs_token` (all mode `600`; the repo-local file is gitignored). The token belongs to `kkazakov`.
-- Authenticate API calls with `Authorization: token $GOGS_TOKEN`. The token can list releases and read repository data.
-- This Gogs build exposes only `GET /repos/:owner/:repo/releases`; `POST /api/v1/repos/.../releases` returns an HTML 404. Create and publish releases in the web UI at `/releases/new`; do not block a release, or claim it is published, just because the API cannot create one.
+- Never commit, print, or paste the Gogs token or web password into chat, commit messages, logs, release bodies, or shell history.
+- **API token:** read from `GOGS_TOKEN`, else `$HOME/.config/cardash/gogs_token`, else `android/keystore/gogs_token` (all mode `600`; the repo-local file is gitignored). It belongs to `kkazakov`. Authenticate with `Authorization: token $GOGS_TOKEN`; it can list releases and read repository data.
+- **Web credentials:** `tool/gogs_release.sh` logs into the web form to publish releases, because this Gogs build only exposes `GET /repos/:owner/:repo/releases` and returns 404 for creating releases over the API. It reads `username=` / `password=` from `GOGS_CREDENTIALS`, else `$HOME/.config/cardash/gogs_credentials`, else `android/keystore/gogs_credentials` (mode `600`, gitignored). The repo-local file is the durable path across Paseo rebuilds.
+- Do not block a release, or claim it is published, just because the API cannot create one. Publish with `./tool/gogs_release.sh ...` or the web UI, then verify the `draft: false` release through the API/page.
 - Use the token only for API operations this Gogs version actually supports.

+ 4 - 4
README.md

@@ -336,10 +336,10 @@ title, artist, progress and pause/resume were verified against a live Chrome
 MediaSession. The OBD hardware path compiles and is covered by the
 parser/economy design, but no ELM327 adapter is available on this machine;
 socket behavior still needs a car test. The 1.0.7 universal APK is built from
-this tree and published; devices on 1.0.6 will pick it up through
-Settings → Updates when the release is published. The emulator has verified the
-in-app update path. 1.0.7 moves the Impeller opt-out into the manifest's
-`<application>` block; Flutter ignored it under `<activity>`.
+this tree and published; devices on 1.0.6 can pick it up through
+Settings → Updates. The emulator has verified the in-app update path. 1.0.7
+moves the Impeller opt-out into the manifest's `<application>` block; Flutter
+ignored it under `<activity>`.
 
 ## Troubleshooting the emulator
 

+ 27 - 0
releases/1.0.7.md

@@ -0,0 +1,27 @@
+CarDash 1.0.7 fixes the emulator crash when opening Settings.
+
+- **Package:** `com.wasp.cardash_launcher`
+- **Version:** 1.0.7 (versionCode 19)
+- **ABIs:** arm64-v8a, armeabi-v7a, x86_64
+- **Signing:** project debug key matching all prior releases
+- **SHA-256:** `5b37116b38683ae5e9c42f36841ca509a417f21164902ffe0a93e7b5b29c61ae`
+
+**Root cause**
+- The `EnableImpeller=false` opt-out was under `<activity>`. Flutter silently ignores that key there, so CarDash had actually been running Impeller.
+- On the 1024×600 SwiftShader emulator, opening Settings killed the emulator process.
+- 1.0.7 moves the key to `<application>`. The app now uses Skia as intended. The Flutter log reports the Impeller opt-out instead of “Using the Impeller rendering backend”.
+
+**Verified on the Android 14 / 1024×600 SwiftShader emulator**
+- Fresh install, open Settings: stays alive.
+- Grant Bluetooth permission: stays alive.
+- Bluetooth off → **Open Bluetooth settings** → enable Bluetooth → return to CarDash: stays alive.
+- Tap **Find** and reopen the card: stays responsive.
+
+**Includes**
+- All 1.0.6 Bluetooth/OBD hardening: binder calls off the main thread, adapter filtering, bounded retries and the **Retry connection** action.
+
+**[Download cardash-android-1.0.7.apk](https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-1.0.7.apk)**
+
+The installed 1.0.6 app can use Settings → Updates to download this APK and open Android package installer. Android requires install confirmation and a one-time unknown-apps allowance; no silent install is attempted. A yellow dot appears on the Settings gear when this release is available.
+
+This build is debug-signed for sideloading/testing, not Play Store distribution. Gogs release attachments are limited to 32 MB, so the 54 MB universal APK is linked directly from the repository.

+ 170 - 0
tool/gogs_release.sh

@@ -0,0 +1,170 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Create and publish a Gogs release through the web form.
+#
+# Gogs's API only exposes GET /repos/:owner/:repo/releases; POST returns 404 on
+# this instance. The web form still accepts a normal user/password session, so
+# this helper logs in locally and posts the release. Credentials are read from
+# a mode-600 file or prompt and are never written to the repository or printed.
+#
+# Usage:
+#   ./tool/gogs_release.sh <tag> <title> <body-file>
+#
+# Example:
+#   ./tool/gogs_release.sh v1.0.7 "CarDash 1.0.7 — Truly disable Impeller" releases/1.0.7.md
+#
+# Optional environment:
+#   GOGS_URL          default https://git.krasi.net
+#   GOGS_REPO         default kkazakov/car-interface-android
+#   GOGS_TARGET       default main
+#   GOGS_USER         skip the username prompt
+#   GOGS_PASSWORD     skip the password prompt (prefer the credentials file)
+#   GOGS_CREDENTIALS  credentials file path (default: $HOME/.config/cardash/gogs_credentials,
+#                     then android/keystore/gogs_credentials)
+#
+# Credentials file format (mode 600, gitignored):
+#   username=kkazakov
+#   password=...
+#
+# If 2FA is enabled, log in through the browser and use the web UI instead.
+
+BASE="${GOGS_URL:-https://git.krasi.net}"
+REPO="${GOGS_REPO:-kkazakov/car-interface-android}"
+TARGET="${GOGS_TARGET:-main}"
+
+if [[ $# -lt 2 || $# -gt 3 ]]; then
+  echo "Usage: $0 <tag> <title> [body-file]" >&2
+  echo "Example: $0 v1.0.7 'CarDash 1.0.7' releases/1.0.7.md" >&2
+  exit 2
+fi
+
+TAG="$1"
+TITLE="$2"
+BODY_FILE="${3:-releases/${TAG#v}.md}"
+
+if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+  echo "Tag must look like v1.2.3 (got: $TAG)." >&2
+  exit 2
+fi
+if [[ ! -f "$BODY_FILE" ]]; then
+  echo "Release body file not found: $BODY_FILE" >&2
+  exit 2
+fi
+
+USER_NAME="${GOGS_USER:-}"
+PASSWORD="${GOGS_PASSWORD:-}"
+
+CRED_FILE="${GOGS_CREDENTIALS:-}"
+if [[ -z "$CRED_FILE" ]]; then
+  for candidate in \
+    "$HOME/.config/cardash/gogs_credentials" \
+    "android/keystore/gogs_credentials"; do
+    if [[ -f "$candidate" ]]; then
+      CRED_FILE="$candidate"
+      break
+    fi
+  done
+fi
+if [[ -n "$CRED_FILE" ]]; then
+  if [[ ! -r "$CRED_FILE" ]]; then
+    echo "Credentials file is not readable: $CRED_FILE" >&2
+    exit 2
+  fi
+  if [[ -z "$USER_NAME" ]]; then
+    USER_NAME="$(sed -n 's/^username=//p' "$CRED_FILE" | head -n 1)"
+  fi
+  if [[ -z "$PASSWORD" ]]; then
+    PASSWORD="$(sed -n 's/^password=//p' "$CRED_FILE" | head -n 1)"
+  fi
+fi
+
+if [[ -z "$USER_NAME" ]]; then
+  read -r -p "Gogs user: " USER_NAME
+fi
+
+if [[ -z "$PASSWORD" ]]; then
+  read -r -s -p "Gogs password for $USER_NAME: " PASSWORD
+  echo
+fi
+if [[ -z "$PASSWORD" ]]; then
+  echo "No password supplied." >&2
+  exit 2
+fi
+
+umask 077
+TMP_DIR="$(mktemp -d)"
+cleanup() {
+  rm -rf "$TMP_DIR"
+}
+trap cleanup EXIT
+
+COOKIES="$TMP_DIR/cookies.txt"
+PASS_FILE="$TMP_DIR/password"
+printf '%s' "$PASSWORD" > "$PASS_FILE"
+unset PASSWORD
+
+csrf_from() {
+  sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$1" | head -n 1
+}
+
+LOGIN_PAGE="$TMP_DIR/login.html"
+if ! curl -fsS -c "$COOKIES" "$BASE/user/login" -o "$LOGIN_PAGE"; then
+  echo "Could not reach $BASE/user/login." >&2
+  exit 1
+fi
+LOGIN_CSRF="$(csrf_from "$LOGIN_PAGE")"
+if [[ -z "$LOGIN_CSRF" ]]; then
+  echo "Could not read the Gogs login form. Is GOGS_URL correct?" >&2
+  exit 1
+fi
+
+LOGIN_CODE="$(curl -sS -b "$COOKIES" -c "$COOKIES" \
+  -o "$TMP_DIR/login-result.html" -w '%{http_code}' \
+  -X POST "$BASE/user/login" \
+  --data-urlencode "_csrf=$LOGIN_CSRF" \
+  --data-urlencode "user_name=$USER_NAME" \
+  --data-urlencode "password@$PASS_FILE" \
+  --data-urlencode 'remember=on')"
+if [[ "$LOGIN_CODE" != "302" ]]; then
+  echo "Gogs login failed (HTTP $LOGIN_CODE). Check the username/password or 2FA." >&2
+  exit 1
+fi
+
+NEW_PAGE="$TMP_DIR/new-release.html"
+if ! curl -fsS -b "$COOKIES" -c "$COOKIES" \
+  "$BASE/$REPO/releases/new" -o "$NEW_PAGE"; then
+  echo "Could not open $BASE/$REPO/releases/new. Is the session valid?" >&2
+  exit 1
+fi
+if ! grep -q 'name="tag_name"' "$NEW_PAGE"; then
+  echo "The release form did not load; the account may lack write access." >&2
+  exit 1
+fi
+RELEASE_CSRF="$(csrf_from "$NEW_PAGE")"
+if [[ -z "$RELEASE_CSRF" ]]; then
+  echo "Could not read the release form CSRF token." >&2
+  exit 1
+fi
+
+RELEASE_CODE="$(curl -sS -b "$COOKIES" -c "$COOKIES" \
+  -D "$TMP_DIR/release-headers.txt" -o "$TMP_DIR/release-result.html" \
+  -w '%{http_code}' \
+  -X POST "$BASE/$REPO/releases/new" \
+  --data-urlencode "_csrf=$RELEASE_CSRF" \
+  --data-urlencode "tag_name=$TAG" \
+  --data-urlencode "tag_target=$TARGET" \
+  --data-urlencode "title=$TITLE" \
+  --data-urlencode "content@$BODY_FILE")"
+
+if [[ "$RELEASE_CODE" != "302" ]]; then
+  echo "Gogs rejected the release (HTTP $RELEASE_CODE)." >&2
+  echo "Check that $TAG does not already exist and that $TARGET is a branch." >&2
+  exit 1
+fi
+
+if grep -q "\"tag_name\": *\"$TAG\"" <(curl -fsS "$BASE/api/v1/repos/$REPO/releases"); then
+  echo "Published $TAG"
+else
+  echo "Release request accepted; verify at $BASE/$REPO/releases"
+fi