diff --git a/.github/scripts/build-release-notes.sh b/.github/scripts/build-release-notes.sh deleted file mode 100644 index 334fb0a5e..000000000 --- a/.github/scripts/build-release-notes.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-only -# Build release notes for a tag: the CHANGELOG section for the version plus the -# screenshots. A composed sheet is used when SCREENSHOT_SHEET_URL is set; -# otherwise the individual images are grouped by color scheme. -set -euo pipefail - -TAG="${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" -REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" -VERSION="${TAG#v}" -TAG_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" -SCREENSHOT_BASE_URL="${SCREENSHOT_BASE_URL:-https://raw.githubusercontent.com/$REPO/$TAG_SHA/website/images}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=./screenshot-sections.sh -source "$SCRIPT_DIR/screenshot-sections.sh" - -{ - awk -v h="## [$VERSION]" ' - index($0, h) == 1 { found = 1 } - found && /^## \[/ && index($0, h) != 1 { exit } - found { print } - ' CHANGELOG.md - - if [ -n "${SCREENSHOT_SHEET_URL:-}" ]; then - echo - echo "## Screenshots" - echo - echo "![App screenshots: light and dark]($SCREENSHOT_SHEET_URL)" - else - emit_screenshots "$SCREENSHOT_BASE_URL" - fi -} > release-notes.md - -if [ ! -s release-notes.md ]; then - echo "See [CHANGELOG.md](https://github.com/$REPO/blob/main/CHANGELOG.md)." > release-notes.md -fi diff --git a/.github/scripts/build-screenshot-sheet.sh b/.github/scripts/build-screenshot-sheet.sh index 25ce9bc90..6f66efcdf 100644 --- a/.github/scripts/build-screenshot-sheet.sh +++ b/.github/scripts/build-screenshot-sheet.sh @@ -3,11 +3,11 @@ # Compose a single screenshot sheet for release notes: light shots in one row, # dark shots in a row below, on a transparent background. # -# Usage: build-screenshot-sheet.sh +# Usage: build-screenshot-sheet.sh set -euo pipefail -OUT="${1:?usage: build-screenshot-sheet.sh }" -DIR="website/images" +SRC="${1:?usage: build-screenshot-sheet.sh }" +OUT="${2:?usage: build-screenshot-sheet.sh }" if command -v magick > /dev/null 2>&1; then MONTAGE="magick montage" @@ -19,14 +19,15 @@ else fi light=() dark=() -for file in "$DIR"/*.png; do +for file in "$SRC"/*.png; do [ -e "$file" ] || continue case "$(basename "$file")" in *-dark.png) dark+=("$file") ;; + *screenshot-sheet.png) ;; *) light+=("$file") ;; esac done -[ "${#light[@]}" -gt 0 ] || { echo "no light screenshots found" >&2; exit 1; } +[ "${#light[@]}" -gt 0 ] || { echo "no light screenshots in $SRC" >&2; exit 1; } tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT diff --git a/.github/scripts/publish-preview-release.sh b/.github/scripts/publish-preview-release.sh index 983e51be9..3e8d5c24c 100644 --- a/.github/scripts/publish-preview-release.sh +++ b/.github/scripts/publish-preview-release.sh @@ -2,31 +2,19 @@ # SPDX-License-Identifier: AGPL-3.0-only # Create or update the rolling "preview" release for the current commit. # -# Run from a checkout of the repository with `dist/app-debug.apk` already built. -# Screenshots ship as release assets: one composed sheet when ImageMagick is -# available, otherwise the individual images. +# Run from a checkout with `dist/app-debug.apk` built and the emulator +# screenshots captured into `screenshots/` (see android.yml). set -euo pipefail REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" -SHORT_SHA="$(git rev-parse --short "$SHA")" TAG="preview" TITLE="Preview build" APK="dist/app-debug.apk" -ASSET_APK="TeslaBatteryBLE-preview-$SHORT_SHA.apk" -SITE_IMAGES_DIR="website/images" -SHEET="screenshot-sheet.png" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=./screenshot-sections.sh -source "$SCRIPT_DIR/screenshot-sections.sh" - -SCREENSHOT_BASE_URL="https://github.com/$REPO/releases/download/$TAG" -SHEET_OK="false" -if bash "$SCRIPT_DIR/build-screenshot-sheet.sh" "$SHEET"; then - SHEET_OK="true" - SCREENSHOT_SHEET_URL="$SCREENSHOT_BASE_URL/$SHEET" -fi +ASSET_APK="TeslaBatteryBLE-preview.apk" +SHEET="screenshots/screenshot-sheet.png" +SHEET_NAME="$(basename "$SHEET")" +SHEET_URL="https://github.com/$REPO/releases/download/$TAG/$SHEET_NAME" [ -f "$APK" ] || { echo "missing $APK" >&2; exit 1; } cp "$APK" "$ASSET_APK" @@ -77,35 +65,21 @@ git push origin "refs/tags/$TAG" --force echo echo "**Full diff**: https://github.com/$REPO/compare/$PREV_TAG...$TAG" fi - if [ "$SHEET_OK" = "true" ]; then + if [ -f "$SHEET" ]; then echo echo "## Screenshots" echo - echo "![App screenshots: light and dark]($SCREENSHOT_SHEET_URL)" - else - emit_screenshots "$SCREENSHOT_BASE_URL" + echo "![App screenshots: light and dark]($SHEET_URL)" fi } > preview-notes.md if gh release view "$TAG" > /dev/null 2>&1; then - # Drop stale SHA-named APK assets from previous runs so old builds never linger. - while read -r name; do - [ -n "$name" ] || continue - gh release delete-asset "$TAG" "$name" --yes || true - done < <(gh release view "$TAG" --json assets --jq '.assets[].name | select(endswith(".apk"))') gh release edit "$TAG" --title "$TITLE" --prerelease --notes-file preview-notes.md else - gh release create "$TAG" --title "$TITLE" --prerelease --notes-file preview-notes.md "$ASSET_APK" + gh release create "$TAG" --title "$TITLE" --prerelease --notes-file preview-notes.md fi gh release upload "$TAG" "$ASSET_APK" --clobber -if [ "$SHEET_OK" = "true" ]; then +if [ -f "$SHEET" ]; then gh release upload "$TAG" "$SHEET" --clobber - # Drop stale individual screenshots now that the sheet is used. - while read -r asset; do - [ -n "$asset" ] || continue - gh release delete-asset "$TAG" "$asset" --yes || true - done < <(gh release view "$TAG" --json assets --jq '.assets[].name | select(endswith(".png")) | select(. != "screenshot-sheet.png")') -elif compgen -G "$SITE_IMAGES_DIR/*.png" > /dev/null; then - gh release upload "$TAG" "$SITE_IMAGES_DIR"/*.png --clobber fi diff --git a/.github/scripts/publish-tag-release.sh b/.github/scripts/publish-tag-release.sh index 73a73b038..29f547c4a 100644 --- a/.github/scripts/publish-tag-release.sh +++ b/.github/scripts/publish-tag-release.sh @@ -1,85 +1,49 @@ #!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-only -# Publish a tagged release. -# -# If the rolling preview release points at this commit, convert it into the -# tagged release (move the release to the new tag, update title/notes, replace -# the asset) instead of deleting it and creating a new one. Otherwise fall -# back to creating a fresh release. -# -# Screenshots ship as release assets: one composed sheet when ImageMagick is -# available, otherwise the individual images. +# Publish a tagged release: the CHANGELOG section plus the screenshot sheet +# captured by CI. Run from a checkout with the debug APK built and the +# emulator screenshots captured into `screenshots/`. set -euo pipefail TAG="${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" -SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" -APK="app/build/outputs/apk/debug/app-debug.apk" -ASSET_APK="TeslaBatteryBLE-${TAG}.apk" +VERSION="${TAG#v}" +APK="dist/TeslaBatteryBLE-${TAG}.apk" NOTES="release-notes.md" -SITE_IMAGES_DIR="website/images" -SHEET="screenshot-sheet.png" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -case "$TAG" in - *-*) PRERELEASE_JSON="true" ;; - *) PRERELEASE_JSON="false" ;; -esac - -[ -f "$APK" ] || { echo "missing $APK" >&2; exit 1; } -cp "$APK" "$ASSET_APK" - -SHEET_OK="false" -if bash "$SCRIPT_DIR/build-screenshot-sheet.sh" "$SHEET"; then - SHEET_OK="true" -fi -export SCREENSHOT_BASE_URL="https://github.com/$REPO/releases/download/$TAG" -if [ "$SHEET_OK" = "true" ]; then - export SCREENSHOT_SHEET_URL="$SCREENSHOT_BASE_URL/$SHEET" -fi -bash "$SCRIPT_DIR/build-release-notes.sh" - -git fetch --tags origin - -REUSED="false" -PREVIEW_ID="$(gh release view preview --json databaseId --jq '.databaseId' 2>/dev/null || true)" -if [ -n "$PREVIEW_ID" ]; then - PREVIEW_SHA="$(git rev-parse -q --verify 'refs/tags/preview^{commit}' 2>/dev/null || true)" - if [ -n "$PREVIEW_SHA" ] && [ "$PREVIEW_SHA" = "$SHA" ]; then - jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$(cat "$NOTES")" \ - --argjson pre "$PRERELEASE_JSON" \ - '{tag_name: $tag, name: $name, prerelease: $pre, body: $body}' > release-payload.json - if gh api -X PATCH "repos/$REPO/releases/$PREVIEW_ID" --input release-payload.json > /dev/null; then - REUSED="true" - echo "Converted the rolling preview release into $TAG." - else - echo "Preview conversion failed; creating a new release instead." - fi +SHEET="screenshots/screenshot-sheet.png" +SHEET_NAME="$(basename "$SHEET")" +SHEET_URL="https://github.com/$REPO/releases/download/$TAG/$SHEET_NAME" + +if [ "$TAG" != "${TAG%-*}" ]; then PRE_FLAG="--prerelease"; else PRE_FLAG=""; fi + +[ -f "$APK" ] || { echo "missing $APK (the build job stages it for tags)" >&2; exit 1; } + +{ + awk -v h="## [$VERSION]" ' + index($0, h) == 1 { found = 1 } + found && /^## \[/ && index($0, h) != 1 { exit } + found { print } + ' CHANGELOG.md + + if [ -f "$SHEET" ]; then + echo + echo "## Screenshots" + echo + echo "![App screenshots: light and dark]($SHEET_URL)" fi +} > "$NOTES" + +if [ ! -s "$NOTES" ]; then + echo "See [CHANGELOG.md](https://github.com/$REPO/blob/main/CHANGELOG.md)." > "$NOTES" fi -if [ "$REUSED" = "true" ]; then - gh release upload "$TAG" "$ASSET_APK" --clobber - while read -r asset; do - [ -n "$asset" ] || continue - gh release delete-asset "$TAG" "$asset" --yes || true - done < <(gh release view "$TAG" --json assets --jq '.assets[].name | select(startswith("TeslaBatteryBLE-preview-"))') - # The release no longer points at the rolling tag. - git push origin ":refs/tags/preview" || true -elif [ "$PRERELEASE_JSON" = "true" ]; then - gh release create "$TAG" --title "$TAG" --prerelease --notes-file "$NOTES" "$ASSET_APK" +if gh release view "$TAG" > /dev/null 2>&1; then + gh release edit "$TAG" --title "$TAG" --notes-file "$NOTES" $PRE_FLAG else - gh release create "$TAG" --title "$TAG" --notes-file "$NOTES" "$ASSET_APK" + gh release create "$TAG" --title "$TAG" --notes-file "$NOTES" $PRE_FLAG fi -if [ "$SHEET_OK" = "true" ]; then +gh release upload "$TAG" "$APK" --clobber +if [ -f "$SHEET" ]; then gh release upload "$TAG" "$SHEET" --clobber - # Drop stale individual screenshots now that the sheet is used. - while read -r asset; do - [ -n "$asset" ] || continue - gh release delete-asset "$TAG" "$asset" --yes || true - done < <(gh release view "$TAG" --json assets --jq '.assets[].name | select(endswith(".png")) | select(. != "screenshot-sheet.png")') -elif compgen -G "$SITE_IMAGES_DIR/*.png" > /dev/null; then - gh release upload "$TAG" "$SITE_IMAGES_DIR"/*.png --clobber fi diff --git a/.github/scripts/screenshot-sections.sh b/.github/scripts/screenshot-sections.sh deleted file mode 100644 index 0689c6f5b..000000000 --- a/.github/scripts/screenshot-sections.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-only -# Emit the "## Screenshots" section as grouped light and dark rows, each sized -# as a percentage of the page width so the group always fits. Used as a -# fallback when no composed screenshot sheet is available. -# -# Usage: emit_screenshots -# The base URL is the release's asset URL (or any prefix); file names are -# appended directly, so it must not end with a slash. -emit_screenshots() { - local base="$1" - local dir="website/images" - local light=() dark=() file - if [ -d "$dir" ]; then - for file in "$dir"/*.png; do - [ -e "$file" ] || continue - case "$(basename "$file")" in - *-dark.png) dark+=("$file") ;; - *) light+=("$file") ;; - esac - done - fi - - if [ "${#light[@]}" -eq 0 ] && [ "${#dark[@]}" -eq 0 ]; then - return 0 - fi - - echo - echo "## Screenshots" - - local line file_name width - if [ "${#light[@]}" -gt 0 ]; then - echo - echo "**Light**" - echo - width=$((90 / ${#light[@]})) - line="" - for file in "${light[@]}"; do - file_name="$(basename "$file")" - line+="\"${file_name%.png}\" " - done - echo "${line% }" - fi - if [ "${#dark[@]}" -gt 0 ]; then - echo - echo "**Dark**" - echo - width=$((90 / ${#dark[@]})) - line="" - for file in "${dark[@]}"; do - file_name="$(basename "$file")" - line+="\"${file_name%.png}\" " - done - echo "${line% }" - fi -} diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index bae43bd42..3d1307b80 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -74,10 +74,46 @@ jobs: name: teslable-debug-apk path: app/build/outputs/apk/debug/*.apk + # Tag releases capture their own screenshots from the simulated car. + - name: Stage the release APK + if: startsWith(github.ref, 'refs/tags/') + run: | + mkdir -p dist + cp app/build/outputs/apk/debug/app-debug.apk "dist/TeslaBatteryBLE-${GITHUB_REF_NAME}.apk" + - name: Install ImageMagick if: startsWith(github.ref, 'refs/tags/') run: sudo apt-get update -qq && sudo apt-get install -y -qq imagemagick + - name: Build the demo APK for screenshots + if: startsWith(github.ref, 'refs/tags/') + run: ./gradlew :app:assembleDebug -PdemoCar=true + + - name: Enable KVM + if: startsWith(github.ref, 'refs/tags/') + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Capture screenshots + if: startsWith(github.ref, 'refs/tags/') + continue-on-error: true + uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + profile: pixel_6 + emulator-boot-timeout: 420 + disable-animations: true + script: bash .github/scripts/capture-screenshots.sh app/build/outputs/apk/debug/app-debug.apk screenshots + + - name: Compose the screenshot sheet + if: startsWith(github.ref, 'refs/tags/') + continue-on-error: true + run: bash .github/scripts/build-screenshot-sheet.sh screenshots screenshots/screenshot-sheet.png + - name: Release on tag if: startsWith(github.ref, 'refs/tags/') env: @@ -107,11 +143,47 @@ jobs: name: teslable-debug-apk path: dist - # Screenshots in the release notes come from the deployed site - # (website/images), so the preview job no longer runs an emulator. + # The preview captures its own screenshots from the simulated car. + - name: Set up JDK 21 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 + with: + distribution: temurin + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6 + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: Build the demo APK for screenshots + run: ./gradlew :app:assembleDebug -PdemoCar=true + - name: Install ImageMagick run: sudo apt-get update -qq && sudo apt-get install -y -qq imagemagick + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Capture screenshots + continue-on-error: true + uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + profile: pixel_6 + emulator-boot-timeout: 420 + disable-animations: true + script: bash .github/scripts/capture-screenshots.sh app/build/outputs/apk/debug/app-debug.apk screenshots + + - name: Compose the screenshot sheet + continue-on-error: true + run: bash .github/scripts/build-screenshot-sheet.sh screenshots screenshots/screenshot-sheet.png + - name: Publish preview release env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/demo-smoke.yml b/.github/workflows/demo-smoke.yml index 40e6bde48..a8cce1248 100644 --- a/.github/workflows/demo-smoke.yml +++ b/.github/workflows/demo-smoke.yml @@ -35,7 +35,8 @@ jobs: demo: name: Simulated car on emulator needs: tests - if: github.event_name != 'pull_request' + # Manual only: release jobs capture screenshots automatically. + if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index c5189470d..1f076812c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ All notable changes to this project are documented here. The format is based on reading, instead of only a connection state - Unpaired car view explains the NFC-card walkthrough before pairing - Wake and command send failures are logged instead of failing silently +- Release screenshots are captured from the simulated car automatically in CI + and attached to each release as one `screenshot-sheet.png`; tag releases are + always created fresh (the rolling preview is no longer converted in place) ## [0.2.0-beta.2] - 2026-10-05 diff --git a/docs/master-plan.md b/docs/master-plan.md index fc5df931c..d4b78b41e 100644 --- a/docs/master-plan.md +++ b/docs/master-plan.md @@ -60,7 +60,7 @@ A trustworthy shell around the existing protocol work. No user-facing features. - [x] CI gates: Android lint, app unit tests, concurrency cancel, least-privilege permissions, Dependabot. - [x] CI hardening: pinned action SHAs (#16); ktlint + detekt gates (#19). - [ ] Required checks on `main` (branch protection deliberately deferred while agents push directly). -- [x] Release engineering: owner keystore + `SIGNING_*` CI secrets; tag releases ship `TeslaBatteryBLE-.apk` with screenshots pinned to the release commit; release checklist documented; the rolling preview is removed when there are no un-released changes. +- [x] Release engineering: owner keystore + `SIGNING_*` CI secrets; tag releases ship `TeslaBatteryBLE-.apk`; every release captures screenshots from the simulated car in CI and attaches one sheet; release checklist documented; the rolling preview is removed when there are no un-released changes. - [x] Docs skeleton: `docs/adr/` (ADR-0001 moved), `docs/protocol/` (transport, session/pairing, domains). - [x] `AGENTS.md` for future sessions. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index f65dc4cfe..f34ba28c6 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -19,9 +19,9 @@ ## Release - [ ] Create and push tag `vX.Y.Z` (e.g. `git tag v0.2.0 && git push origin v0.2.0`). -- [ ] CI builds the APK, attaches it as `TeslaBatteryBLE-.apk`, sets the release title, and marks tags containing `-` as prereleases. -- [ ] Release notes come from the matching `CHANGELOG.md` section plus a single screenshot sheet attached to the release (immutable). -- [ ] Verify the release page: correct tag/version, notes, and installable APK artifact. +- [ ] CI builds the APK, attaches it as `TeslaBatteryBLE-.apk`, captures screenshots from the simulated car on the emulator, composes one `screenshot-sheet.png`, and attaches it. +- [ ] CI sets the release title, marks tags containing `-` as prereleases, and writes notes from the matching `CHANGELOG.md` section plus the sheet. +- [ ] Verify the release page: correct tag/version, notes, screenshots, and installable APK artifact. - [ ] The rolling preview is removed automatically when the tagged commit matches it; otherwise it remains until the next un-released push. ## Current status