on main help sign in
Add a release workflow that publishes versioned APKs
e4d8565Claude
.github/workflows/build.yml+9
@@ -20,6 +20,10 @@ on:
20 apk:
20 apk:
21 type: boolean
21 type: boolean
22 default: false
22 default: false
23+ build_number:
24+ description: MultiGram build number (0 to 9), the fourth part of the version (release.yml sets it)
25+ type: string
26+ default: '0'
23
27
24permissions:
28permissions:
25 contents: read
29 contents: read
@@ -94,8 +98,13 @@ jobs:
94 KEYSTORE_PASSWORD: ${{ secrets.MULTIGRAM_KEYSTORE_PASSWORD }}
98 KEYSTORE_PASSWORD: ${{ secrets.MULTIGRAM_KEYSTORE_PASSWORD }}
95 KEY_ALIAS: ${{ secrets.MULTIGRAM_KEY_ALIAS }}
99 KEY_ALIAS: ${{ secrets.MULTIGRAM_KEY_ALIAS }}
96 KEY_PASSWORD: ${{ secrets.MULTIGRAM_KEY_PASSWORD }}
100 KEY_PASSWORD: ${{ secrets.MULTIGRAM_KEY_PASSWORD }}
101+ BUILD_NUMBER: ${{ inputs.build_number }}
97 run: |
102 run: |
98 vars=gradle.properties
103 vars=gradle.properties
104+ # Forkgram's versioning: versionName APP_VERSION_NAME.N and versionCode
105+ # (APP_VERSION_CODE * 10 + N) * 10 + the flavor's ABI code, with N = ADDITIONAL_BUILD_NUMBER.
106+ case "${BUILD_NUMBER:-0}" in [0-9]) ;; *) echo "::error::build_number must be one digit"; exit 1;; esac
107+ echo "ADDITIONAL_BUILD_NUMBER=${BUILD_NUMBER:-0}" >> $vars
99 echo "org.gradle.workers.max=1" >> $vars
108 echo "org.gradle.workers.max=1" >> $vars
100 # Forkgram asks for an 8 GB heap, sized for the bigger runners of public repositories.
109 # Forkgram asks for an 8 GB heap, sized for the bigger runners of public repositories.
101 echo "org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8" >> $vars
110 echo "org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8" >> $vars
.github/workflows/release.ymlnew file+216
@@ -0,0 +1,216 @@
1+name: Release MultiGram
2+run-name: Release ${{ inputs.ref }}${{ inputs.dry_run && ' (dry run)' || '' }}
3+
4+# Builds the release APK of a MultiGram commit and publishes it as a GitHub Release.
5+#
6+# Version: Forkgram's own scheme, APP_VERSION_NAME.N (for example 12.10.6.1), where APP_VERSION_NAME
7+# comes from gradle.properties of the Forkgram snapshot and N (1 to 9) counts MultiGram releases on
8+# that Forkgram version. The Android versionCode is (APP_VERSION_CODE * 10 + N) * 10 + 9, so every
9+# release installs as an update over the one before, and a new Forkgram version starts again at N = 1.
10+# The tag is the version itself, like Forkgram's tags.
11+
12+on:
13+ workflow_dispatch:
14+ inputs:
15+ ref:
16+ description: Branch, tag or commit to release
17+ default: multigram
18+ required: true
19+ number:
20+ description: MultiGram build number for this Forkgram version (1 to 9). Empty picks the next free one.
21+ required: false
22+ default: ''
23+ prerelease:
24+ description: Mark it as a pre-release (always the case without your own signing key)
25+ type: boolean
26+ default: false
27+ dry_run:
28+ description: Only work out the version and the release notes; build and publish nothing
29+ type: boolean
30+ default: false
31+
32+permissions:
33+ contents: read
34+
35+concurrency:
36+ group: release
37+ cancel-in-progress: false
38+
39+jobs:
40+ prepare:
41+ name: Work out the version
42+ runs-on: ubuntu-latest
43+ outputs:
44+ sha: ${{ steps.v.outputs.sha }}
45+ number: ${{ steps.v.outputs.number }}
46+ version: ${{ steps.v.outputs.version }}
47+ version_code: ${{ steps.v.outputs.version_code }}
48+ prerelease: ${{ steps.v.outputs.prerelease }}
49+ notes: ${{ steps.v.outputs.notes }}
50+ steps:
51+ - uses: actions/checkout@v4
52+ with:
53+ ref: ${{ inputs.ref }}
54+ fetch-depth: 0
55+ - name: Version, secrets and release notes
56+ id: v
57+ env:
58+ NUMBER: ${{ inputs.number }}
59+ PRERELEASE: ${{ inputs.prerelease }}
60+ DRY_RUN: ${{ inputs.dry_run }}
61+ APP_ID: ${{ secrets.MULTIGRAM_APP_ID }}
62+ APP_HASH: ${{ secrets.MULTIGRAM_APP_HASH }}
63+ KEYSTORE_BASE64: ${{ secrets.MULTIGRAM_KEYSTORE_BASE64 }}
64+ run: |
65+ set -euo pipefail
66+ fail() { echo "::error::$1"; exit 1; }
67+ sha=$(git rev-parse HEAD)
68+ name=$(sed -n 's/^APP_VERSION_NAME=//p' gradle.properties | tail -n 1)
69+ code=$(sed -n 's/^APP_VERSION_CODE=//p' gradle.properties | tail -n 1)
70+ [[ "$name" =~ ^[0-9]+(\.[0-9]+)*$ ]] || fail "APP_VERSION_NAME in gradle.properties is '$name', not a version number."
71+ [[ "$code" =~ ^[0-9]+$ ]] || fail "APP_VERSION_CODE in gradle.properties is '$code', not a number."
72+
73+ # The next free N for this Forkgram version, from the tags that already exist.
74+ last=$(git tag -l "$name.*" | sed -n "s/^${name//./\\.}\.\([0-9]\)$/\1/p" | sort -n | tail -n 1)
75+ if [ -n "$NUMBER" ]; then
76+ n=$NUMBER
77+ else
78+ n=$(( ${last:-0} + 1 ))
79+ fi
80+ [[ "$n" =~ ^[1-9]$ ]] || fail "The MultiGram build number must be 1 to 9 (got '$n'). Nine releases on Forkgram $name are the most its version codes allow; wait for the next Forkgram version."
81+ version="$name.$n"
82+ if git rev-parse -q --verify "refs/tags/$version" >/dev/null; then
83+ fail "Tag $version already exists: MultiGram $version is already released. Leave the number empty to take the next free one."
84+ fi
85+ if [ -n "$last" ] && [ "$n" -le "$last" ]; then
86+ echo "::warning::$name.$last is already released, so $version has a lower version code and will not install as an update over it."
87+ fi
88+ version_code=$(( (code * 10 + n) * 10 + 9 ))
89+
90+ # What it is built on, from the snapshot commit the sync made.
91+ snap=$(git log -n 1 --format=%H --grep='^Forkgram-Version: ' HEAD || true)
92+ fg_version=unknown; tg_version=unknown
93+ if [ -n "$snap" ]; then
94+ msg=$(git log -1 --format=%B "$snap")
95+ fg_version=$(printf '%s\n' "$msg" | sed -n 's/^Forkgram-Version: //p' | head -n 1)
96+ tg_version=$(printf '%s\n' "$msg" | sed -n 's/^Telegram-Base: .*(update to \([0-9.]*\).*/\1/p' | head -n 1)
97+ fg_version=${fg_version:-unknown}; tg_version=${tg_version:-unknown}
98+ fi
99+
100+ if [ -z "$APP_ID" ] || [ -z "$APP_HASH" ]; then
101+ m="The MULTIGRAM_APP_ID and MULTIGRAM_APP_HASH secrets are not set, so the APK could not log in. Add them (Settings > Secrets and variables > Actions) and run this again."
102+ if [ "$DRY_RUN" = true ]; then echo "::warning::$m"; else fail "$m"; fi
103+ fi
104+ signed=true
105+ [ -n "$KEYSTORE_BASE64" ] || signed=false
106+ prerelease=$PRERELEASE
107+ if [ "$signed" = false ]; then
108+ prerelease=true
109+ echo "::warning::No MULTIGRAM_KEYSTORE_BASE64 secret: MultiGram $version will be signed with Forkgram's public test key and published as a pre-release."
110+ fi
111+
112+ notes=$(mktemp)
113+ {
114+ echo "MultiGram $version is Forkgram $fg_version (Telegram $tg_version) with the MultiGram changes listed below."
115+ echo
116+ if [ "$signed" = true ]; then
117+ echo "**Signing:** signed with the MultiGram release key. It installs as an update over earlier MultiGram releases signed with the same key."
118+ else
119+ echo "**Signing: public test key.** This APK is signed with Forkgram's public test key (\`TMessagesProj/config/test.keystore\`), because the repository has no \`MULTIGRAM_KEYSTORE_BASE64\` secret. Anyone can sign an \"update\" for it, and a later release signed with a real key cannot update it. Use it for testing only."
120+ fi
121+ echo
122+ echo "**Version:** $version (Android version code $version_code). Built from $sha."
123+ echo
124+ echo "**MultiGram changes on top of Forkgram $fg_version:**"
125+ echo
126+ if [ -n "$snap" ]; then
127+ git log --reverse --format='- %s (%h)' "$snap..HEAD"
128+ else
129+ echo "- (no Forkgram snapshot found in the history)"
130+ fi
131+ } > "$notes"
132+
133+ {
134+ echo "sha=$sha"
135+ echo "number=$n"
136+ echo "version=$version"
137+ echo "version_code=$version_code"
138+ echo "prerelease=$prerelease"
139+ echo "notes<<MULTIGRAM_NOTES_EOF"
140+ cat "$notes"
141+ echo "MULTIGRAM_NOTES_EOF"
142+ } >> "$GITHUB_OUTPUT"
143+ {
144+ if [ "$DRY_RUN" = true ]; then echo "## MultiGram $version (dry run: nothing built or published)"; else echo "## MultiGram $version"; fi
145+ echo
146+ echo "Pre-release: $prerelease"
147+ echo
148+ cat "$notes"
149+ } >> "$GITHUB_STEP_SUMMARY"
150+ cat "$notes"
151+
152+ build:
153+ name: Build
154+ needs: prepare
155+ if: ${{ !inputs.dry_run }}
156+ uses: ./.github/workflows/build.yml
157+ with:
158+ ref: ${{ needs.prepare.outputs.sha }}
159+ apk: true
160+ build_number: ${{ needs.prepare.outputs.number }}
161+ secrets: inherit
162+
163+ publish:
164+ name: Publish the release
165+ needs: [prepare, build]
166+ runs-on: ubuntu-latest
167+ permissions:
168+ contents: write
169+ steps:
170+ - uses: actions/download-artifact@v4
171+ with:
172+ name: multigram-apk
173+ path: apk
174+ - name: Check the APK and publish it
175+ env:
176+ GH_TOKEN: ${{ github.token }}
177+ SHA: ${{ needs.prepare.outputs.sha }}
178+ VERSION: ${{ needs.prepare.outputs.version }}
179+ VERSION_CODE: ${{ needs.prepare.outputs.version_code }}
180+ PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
181+ NOTES: ${{ needs.prepare.outputs.notes }}
182+ run: |
183+ set -euo pipefail
184+ fail() { echo "::error::$1"; exit 1; }
185+ shopt -s nullglob
186+ apks=(apk/*.apk)
187+ [ "${#apks[@]}" -eq 1 ] || fail "Expected one APK in the build artifact, found ${#apks[@]}."
188+ file="MultiGram-$VERSION.apk"
189+ mv "${apks[0]}" "$file"
190+
191+ tools=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n 1)
192+ badging=$("$tools/aapt2" dump badging "$file" | head -n 1)
193+ echo "$badging"
194+ got_name=$(printf '%s' "$badging" | sed -n "s/.* versionName='\([^']*\)'.*/\1/p")
195+ got_code=$(printf '%s' "$badging" | sed -n "s/.* versionCode='\([^']*\)'.*/\1/p")
196+ [ "$got_name" = "$VERSION" ] || fail "The APK says versionName $got_name, expected $VERSION."
197+ [ "$got_code" = "$VERSION_CODE" ] || fail "The APK says versionCode $got_code, expected $VERSION_CODE."
198+ cert=$("$tools/apksigner" verify --print-certs "$file" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p' | head -n 1)
199+ [ -n "$cert" ] || fail "apksigner could not verify the APK's signature."
200+ sha256sum "$file" > SHA256SUMS
201+
202+ {
203+ printf '%s\n' "$NOTES"
204+ echo
205+ echo "**Checksums:** \`$(cut -d' ' -f1 SHA256SUMS)\` ($file, SHA-256). Signing certificate SHA-256: \`$cert\`."
206+ } > notes.md
207+
208+ flags=()
209+ if [ "$PRERELEASE" = true ]; then flags+=(--prerelease); fi
210+ gh release create "$VERSION" "$file" SHA256SUMS \
211+ --repo "$GITHUB_REPOSITORY" \
212+ --target "$SHA" \
213+ --title "MultiGram $VERSION" \
214+ --notes-file notes.md \
215+ "${flags[@]}"
216+ echo "Published https://github.com/$GITHUB_REPOSITORY/releases/tag/$VERSION" | tee -a "$GITHUB_STEP_SUMMARY"