Skip to content

Commit 4b689e2

Browse files
authored
Merge pull request #3 from welldone-tech/release-automation
ci: add artifact-specific release workflows
2 parents 8115ff3 + a493888 commit 4b689e2

8 files changed

Lines changed: 691 additions & 0 deletions
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Release Android App
2+
3+
on:
4+
push:
5+
tags:
6+
- 'android-v*.*.*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build and release Android app
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
20+
- name: Set up JDK
21+
uses: actions/setup-java@v4
22+
with:
23+
distribution: temurin
24+
java-version: '17'
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.13'
30+
31+
- name: Set up Android SDK
32+
uses: android-actions/setup-android@v3
33+
with:
34+
accept-android-sdk-licenses: false
35+
36+
- name: Set up Gradle caching
37+
uses: gradle/actions/setup-gradle@v4
38+
39+
- name: Validate tag format
40+
run: |
41+
if ! printf %s "$GITHUB_REF_NAME" | grep -Eq '^android-v[0-9]+\.[0-9]+\.[0-9]+$'; then
42+
printf 'Tag %s must match android-vX.Y.Z\n' "$GITHUB_REF_NAME" >&2
43+
exit 1
44+
fi
45+
46+
- name: Extract version from tag
47+
run: printf 'VERSION=%s\n' "${GITHUB_REF_NAME#android-v}" >> "$GITHUB_ENV"
48+
49+
- name: Validate app version matches tag
50+
run: |
51+
app_version="$(python3 -c 'from pathlib import Path; content = Path("app/build.gradle.kts").read_text(encoding="utf-8"); print(content.split("versionName = \"", 1)[1].split("\"", 1)[0])')"
52+
if [ "$app_version" != "$VERSION" ]; then
53+
printf 'Tag version %s does not match app version %s\n' "$VERSION" "$app_version" >&2
54+
exit 1
55+
fi
56+
57+
- name: Accept Android SDK licenses
58+
run: yes | sdkmanager --licenses > /dev/null
59+
60+
- name: Install Android SDK packages
61+
run: sdkmanager --install "platforms;android-35" "build-tools;35.0.0" "platform-tools"
62+
63+
- name: Reconstruct signing material
64+
env:
65+
RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
66+
RELEASE_KEYSTORE_PROPERTIES: ${{ secrets.RELEASE_KEYSTORE_PROPERTIES }}
67+
run: |
68+
if [ -z "$RELEASE_KEYSTORE_BASE64" ]; then
69+
printf 'Missing required secret: %s\n' "RELEASE_KEYSTORE_BASE64" >&2
70+
exit 1
71+
fi
72+
if [ -z "$RELEASE_KEYSTORE_PROPERTIES" ]; then
73+
printf 'Missing required secret: %s\n' "RELEASE_KEYSTORE_PROPERTIES" >&2
74+
exit 1
75+
fi
76+
printf %s "$RELEASE_KEYSTORE_BASE64" | base64 --decode > release.keystore
77+
printf %s "$RELEASE_KEYSTORE_PROPERTIES" > keystore.properties
78+
79+
- name: Create local.properties
80+
run: printf 'sdk.dir=%s\n' "$ANDROID_SDK_ROOT" > local.properties
81+
82+
- name: Make Gradle wrapper executable
83+
run: chmod +x ./gradlew
84+
85+
- name: Build release APK
86+
run: ./gradlew assembleRelease
87+
88+
- name: Capture release artifact paths
89+
run: |
90+
artifact_path="app/build/outputs/apk/release/app-release.apk"
91+
printf 'ARTIFACT_PATH=%s\n' "$artifact_path" >> "$GITHUB_ENV"
92+
printf 'ARTIFACT_NAME=%s\n' "$(basename "$artifact_path")" >> "$GITHUB_ENV"
93+
printf 'ARTIFACT_DIR=%s\n' "$(dirname "$artifact_path")" >> "$GITHUB_ENV"
94+
printf 'CHECKSUM_PATH=%s/%s.sha256\n' "$(dirname "$artifact_path")" "$(basename "$artifact_path")" >> "$GITHUB_ENV"
95+
96+
- name: Generate sha256
97+
run: |
98+
cd "$ARTIFACT_DIR"
99+
shasum -a 256 "$ARTIFACT_NAME" > "$ARTIFACT_NAME.sha256"
100+
101+
- name: Create or update GitHub release
102+
env:
103+
GH_TOKEN: ${{ github.token }}
104+
run: |
105+
if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
106+
gh release create "$GITHUB_REF_NAME" --title "Android app v$VERSION"
107+
fi
108+
gh release upload "$GITHUB_REF_NAME" \
109+
"$ARTIFACT_PATH" \
110+
"$CHECKSUM_PATH" \
111+
--clobber
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Release Electrum Plugin
2+
3+
on:
4+
push:
5+
tags:
6+
- 'electrum-v*.*.*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build and release Electrum plugin
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
20+
- name: Validate tag format
21+
run: |
22+
if ! printf %s "$GITHUB_REF_NAME" | grep -Eq '^electrum-v[0-9]+\.[0-9]+\.[0-9]+$'; then
23+
printf 'Tag %s must match electrum-vX.Y.Z\n' "$GITHUB_REF_NAME" >&2
24+
exit 1
25+
fi
26+
27+
- name: Extract version from tag
28+
run: printf 'VERSION=%s\n' "${GITHUB_REF_NAME#electrum-v}" >> "$GITHUB_ENV"
29+
30+
- name: Validate manifest version matches tag
31+
run: |
32+
manifest_version="$(python3 -c 'import json; print(json.load(open("nostr_signer/manifest.json", encoding="utf-8"))["version"])')"
33+
if [ "$manifest_version" != "$VERSION" ]; then
34+
printf 'Tag version %s does not match manifest version %s\n' "$VERSION" "$manifest_version" >&2
35+
exit 1
36+
fi
37+
38+
- name: Package Electrum plugin
39+
run: |
40+
artifact_path="$(bash scripts/package_electrum_plugin.sh)"
41+
case "$artifact_path" in
42+
*.zip) ;;
43+
*)
44+
printf 'Expected zip artifact, got %s\n' "$artifact_path" >&2
45+
exit 1
46+
;;
47+
esac
48+
printf 'ARTIFACT_PATH=%s\n' "$artifact_path" >> "$GITHUB_ENV"
49+
printf 'ARTIFACT_NAME=%s\n' "$(basename "$artifact_path")" >> "$GITHUB_ENV"
50+
printf 'ARTIFACT_DIR=%s\n' "$(dirname "$artifact_path")" >> "$GITHUB_ENV"
51+
printf 'CHECKSUM_PATH=%s/%s.sha256\n' "$(dirname "$artifact_path")" "$(basename "$artifact_path")" >> "$GITHUB_ENV"
52+
53+
- name: Generate sha256
54+
run: |
55+
cd "$ARTIFACT_DIR"
56+
shasum -a 256 "$ARTIFACT_NAME" > "$ARTIFACT_NAME.sha256"
57+
58+
- name: Create or update GitHub release
59+
env:
60+
GH_TOKEN: ${{ github.token }}
61+
run: |
62+
if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
63+
gh release create "$GITHUB_REF_NAME" --title "Electrum plugin v$VERSION"
64+
fi
65+
gh release upload "$GITHUB_REF_NAME" \
66+
"$ARTIFACT_PATH" \
67+
"$CHECKSUM_PATH" \
68+
--clobber

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ keyPassword=<password>
7777

7878
The release keystore must be backed up separately — losing it means the release app cannot be updated in place.
7979

80+
## Release Artifacts
81+
82+
This repository has two releasable artifacts:
83+
84+
- Android app APK, versioned from `app/build.gradle.kts` `versionName`
85+
- Electrum plugin zip, versioned from `nostr_signer/manifest.json` `version`
86+
87+
Release tags are artifact-specific:
88+
89+
- `android-vX.Y.Z`
90+
- `electrum-vX.Y.Z`
91+
92+
Both GitHub release workflows publish the built artifact and a companion `.sha256` file. The Electrum plugin release path packages the zip with `scripts/package_electrum_plugin.sh`.
93+
8094
## Architecture
8195

8296
**Two-language bridge pattern:** Kotlin handles UI, USB, Android lifecycle, and transaction broadcasting. Python handles Bitcoin signing logic (PSBT parsing, trezorlib signing). They communicate via Chaquopy.

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,20 @@ GitHub Actions runs the fast automated checks on every push and pull request:
230230

231231
Android instrumentation tests run in a separate workflow via manual trigger only. That keeps normal PR feedback fast while still allowing full emulator coverage on demand.
232232

233+
### Releases
234+
235+
This repo ships two releasable artifacts:
236+
237+
- Android app release APK
238+
- Electrum plugin zip (`nostr_signer`)
239+
240+
Release automation is tag-driven:
241+
242+
- `android-vX.Y.Z` builds and publishes the Android release from `app/build.gradle.kts` `versionName`
243+
- `electrum-vX.Y.Z` builds and publishes the Electrum plugin from `nostr_signer/manifest.json` `version`
244+
245+
Both release workflows generate a `.sha256` file next to the built artifact and upload the artifact plus its checksum to the matching GitHub release. The Electrum plugin is packaged by `scripts/package_electrum_plugin.sh`.
246+
233247
### Desktop Signing with Real Trezor
234248

235249
You can test the full signing flow on your Mac without deploying to Android. Requires `libusb` (`brew install libusb`).

scripts/package_electrum_plugin.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
6+
MANIFEST_PATH="$ROOT_DIR/nostr_signer/manifest.json"
7+
VERSION=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["version"])' "$MANIFEST_PATH")
8+
ARTIFACT_DIR="$ROOT_DIR/dist"
9+
ARTIFACT_PATH="$ARTIFACT_DIR/nostr_signer-v${VERSION}.zip"
10+
11+
mkdir -p "$ARTIFACT_DIR"
12+
rm -f "$ARTIFACT_PATH"
13+
14+
(
15+
cd "$ROOT_DIR"
16+
zip -X -qr "$ARTIFACT_PATH" nostr_signer
17+
)
18+
19+
printf '%s\n' "$ARTIFACT_PATH"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pathlib import Path
2+
import json
3+
import shutil
4+
import subprocess
5+
import zipfile
6+
7+
8+
ROOT_DIR = Path(__file__).resolve().parent.parent
9+
MANIFEST_PATH = ROOT_DIR / "nostr_signer/manifest.json"
10+
DIST_DIR = ROOT_DIR / "dist"
11+
12+
13+
def test_package_electrum_plugin_creates_expected_zip() -> None:
14+
version = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))["version"]
15+
expected_artifact_path = DIST_DIR / f"nostr_signer-v{version}.zip"
16+
17+
shutil.rmtree(DIST_DIR, ignore_errors=True)
18+
19+
completed = subprocess.run(
20+
["bash", "scripts/package_electrum_plugin.sh"],
21+
check=True,
22+
capture_output=True,
23+
text=True,
24+
cwd=ROOT_DIR,
25+
)
26+
27+
assert completed.stdout.strip() == str(expected_artifact_path)
28+
assert expected_artifact_path.is_file()
29+
30+
with zipfile.ZipFile(expected_artifact_path) as artifact_zip:
31+
names = artifact_zip.namelist()
32+
33+
assert "nostr_signer/" in names
34+
assert "nostr_signer/manifest.json" in names
35+
assert "nostr_signer/qt.py" in names
36+
assert all(name.startswith("nostr_signer/") for name in names)

0 commit comments

Comments
 (0)