mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-26 23:33:02 +08:00
[ci] sign Android release APKs with a fixed publishing key
This commit is contained in:
@@ -93,6 +93,26 @@ class ManifestTest(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self.merge(tag, downloads)
|
self.merge(tag, downloads)
|
||||||
|
|
||||||
|
def test_signed_android_release_updates_existing_unsigned_download(self):
|
||||||
|
unsigned = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk")
|
||||||
|
current = self.merge("android-v1.6.0-20261002", unsigned)
|
||||||
|
signed = self.files("crossdesk-android-arm64-v1.6.0-20261002.apk")
|
||||||
|
result = self.merge("android-v1.6.0-20261002", signed, current)
|
||||||
|
self.assertEqual(result["downloads"]["android-arm64"]["url"],
|
||||||
|
"https://downloads.crossdesk.cn/crossdesk-android-arm64-v1.6.0-20261002.apk")
|
||||||
|
self.assertEqual(result["version"], self.current["version"])
|
||||||
|
self.assertEqual(result["downloads"]["windows-x64"], self.current["downloads"]["windows-x64"])
|
||||||
|
self.assertNotIn("platforms", result)
|
||||||
|
|
||||||
|
def test_signed_android_is_preferred_when_release_retains_unsigned_apk(self):
|
||||||
|
downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk",
|
||||||
|
"crossdesk-android-arm64-v1.6.0-20261002.apk")
|
||||||
|
self.assertEqual(downloads["android-arm64"]["filename"],
|
||||||
|
"crossdesk-android-arm64-v1.6.0-20261002.apk")
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261001.apk",
|
||||||
|
"crossdesk-android-arm64-v1.6.0-20261002.apk")
|
||||||
|
|
||||||
def test_invalid_current_manifest_is_rejected(self):
|
def test_invalid_current_manifest_is_rejected(self):
|
||||||
downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk")
|
downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk")
|
||||||
for current in ([], {}, {"downloads": []}):
|
for current in ([], {}, {"downloads": []}):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ ARTIFACTS = {
|
|||||||
"linux-amd64": ("linux-amd64", "deb"),
|
"linux-amd64": ("linux-amd64", "deb"),
|
||||||
"linux-arm64": ("linux-arm64", "deb"),
|
"linux-arm64": ("linux-arm64", "deb"),
|
||||||
"ios-arm64": ("ios-arm64-unsigned", "zip"),
|
"ios-arm64": ("ios-arm64-unsigned", "zip"),
|
||||||
"android-arm64": ("android-arm64-unsigned", "apk"),
|
"android-arm64": ("android-arm64(?:-unsigned)?", "apk"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -52,9 +52,15 @@ def artifact_downloads(directory):
|
|||||||
match = re.fullmatch(rf"crossdesk-{prefix}-v([0-9][0-9.\-]*)\.{extension}", path.name)
|
match = re.fullmatch(rf"crossdesk-{prefix}-v([0-9][0-9.\-]*)\.{extension}", path.name)
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
if key in downloads:
|
|
||||||
raise ValueError(f"Multiple release artifacts for {key}")
|
|
||||||
metadata = version_info(match[1])
|
metadata = version_info(match[1])
|
||||||
|
if key in downloads:
|
||||||
|
previous = downloads[key]
|
||||||
|
if (key != "android-arm64" or previous["version"] != metadata["version"]
|
||||||
|
or ("-unsigned-" in previous["filename"]) == ("-unsigned-" in path.name)):
|
||||||
|
raise ValueError(f"Multiple release artifacts for {key}")
|
||||||
|
# Older releases may retain their unsigned APK after adding a signed one.
|
||||||
|
if "-unsigned-" in path.name:
|
||||||
|
continue
|
||||||
downloads[key] = {"url": DOWNLOAD_BASE + path.name, "filename": path.name,
|
downloads[key] = {"url": DOWNLOAD_BASE + path.name, "filename": path.name,
|
||||||
"version": metadata["version"], "releaseDate": metadata["releaseDate"]}
|
"version": metadata["version"], "releaseDate": metadata["releaseDate"]}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ on:
|
|||||||
- ".github/workflows/build-android.yml"
|
- ".github/workflows/build-android.yml"
|
||||||
- ".github/workflows/build.yml"
|
- ".github/workflows/build.yml"
|
||||||
- ".github/workflows/release-mobile.yml"
|
- ".github/workflows/release-mobile.yml"
|
||||||
|
- ".github/workflows/sign-android.yml"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- "LICENSE"
|
- "LICENSE"
|
||||||
- "apps/android/**"
|
- "apps/android/**"
|
||||||
|
|||||||
@@ -556,10 +556,23 @@ jobs:
|
|||||||
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
|
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
|
||||||
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
|
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
|
||||||
|
|
||||||
|
sign-android:
|
||||||
|
name: Sign Android release
|
||||||
|
needs: build-android
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: ./.github/workflows/sign-android.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.build-android.outputs.version }}
|
||||||
|
secrets:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [build-linux, build-macos, build-ios, build-android, build-windows-x64]
|
needs: [build-linux, build-macos, build-ios, build-android, sign-android, build-windows-x64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: publish-release-metadata
|
group: publish-release-metadata
|
||||||
@@ -631,7 +644,7 @@ jobs:
|
|||||||
[iOS source, modification and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/ios/README.md).
|
[iOS source, modification and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/ios/README.md).
|
||||||
The iOS archive also contains the exact source revision and third-party source catalog with archive checksums.
|
The iOS archive also contains the exact source revision and third-party source catalog with archive checksums.
|
||||||
[Android source and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/android/README.md).
|
[Android source and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/android/README.md).
|
||||||
The Android arm64 Release APK is unsigned; sign it with your own key before installation.
|
The Android arm64 Release APK is signed with the CrossDesk publishing key and can be installed directly.
|
||||||
It includes the exact application and MiniRTC revisions, license documents and native build recipes.
|
It includes the exact application and MiniRTC revisions, license documents and native build recipes.
|
||||||
|
|
||||||
## Code signing policy
|
## Code signing policy
|
||||||
@@ -647,7 +660,7 @@ jobs:
|
|||||||
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||||
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||||
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
||||||
cp artifacts/crossdesk-android-arm64-unsigned-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-unsigned-${{ needs.build-android.outputs.version }}.apk
|
cp artifacts/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}.apk
|
||||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
||||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
source_tag: ${{ needs.prepare.outputs.tag }}
|
source_tag: ${{ needs.prepare.outputs.tag }}
|
||||||
|
|
||||||
|
sign-android:
|
||||||
|
name: Sign Android release
|
||||||
|
needs: [prepare, build-android]
|
||||||
|
if: needs.prepare.outputs.platform == 'android'
|
||||||
|
uses: ./.github/workflows/sign-android.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.build-android.outputs.version }}
|
||||||
|
secrets:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish mobile release
|
name: Publish mobile release
|
||||||
needs: [prepare, build-ios, build-android]
|
needs: [prepare, build-ios, build-android, sign-android]
|
||||||
if: ${{ !cancelled() && (needs.build-ios.result == 'success' || needs.build-android.result == 'success') }}
|
if: ${{ !cancelled() && (needs.build-ios.result == 'success' || needs.sign-android.result == 'success') }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -102,7 +115,7 @@ jobs:
|
|||||||
- name: Download mobile release artifact
|
- name: Download mobile release artifact
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: crossdesk-${{ env.PLATFORM }}-arm64-unsigned-${{ env.VERSION }}
|
name: crossdesk-${{ env.PLATFORM }}-arm64${{ env.PLATFORM == 'ios' && '-unsigned' || '' }}-${{ env.VERSION }}
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Prepare release notes
|
- name: Prepare release notes
|
||||||
@@ -113,7 +126,7 @@ jobs:
|
|||||||
SIGNING_NOTE="The arm64 ZIP contains an unsigned app, source metadata, third-party sources and any generated dSYM. Sign the app before device installation."
|
SIGNING_NOTE="The arm64 ZIP contains an unsigned app, source metadata, third-party sources and any generated dSYM. Sign the app before device installation."
|
||||||
else
|
else
|
||||||
PLATFORM_NAME="Android"
|
PLATFORM_NAME="Android"
|
||||||
SIGNING_NOTE="The arm64 Release APK is unsigned. Sign it with your own key before installation. It includes source metadata, license documents and native build recipes."
|
SIGNING_NOTE="The arm64 Release APK is signed with the CrossDesk publishing key and can be installed directly. It includes source metadata, license documents and native build recipes."
|
||||||
fi
|
fi
|
||||||
cat > "${RUNNER_TEMP}/mobile-release-notes.md" << EOF
|
cat > "${RUNNER_TEMP}/mobile-release-notes.md" << EOF
|
||||||
${PLATFORM_NAME} ${VERSION}
|
${PLATFORM_NAME} ${VERSION}
|
||||||
@@ -179,4 +192,4 @@ jobs:
|
|||||||
- name: Summarize release
|
- name: Summarize release
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "Published [${SOURCE_TAG}](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${SOURCE_TAG}), uploaded its unsigned ${PLATFORM} artifact to the download server, and updated version.json." >> "${GITHUB_STEP_SUMMARY}"
|
echo "Published [${SOURCE_TAG}](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${SOURCE_TAG}), uploaded its ${PLATFORM} artifact to the download server, and updated version.json." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
name: Sign Android Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Android artifact version, for example v1.6.0-20261002."
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
ANDROID_KEYSTORE_BASE64:
|
||||||
|
required: true
|
||||||
|
ANDROID_KEYSTORE_PASSWORD:
|
||||||
|
required: true
|
||||||
|
ANDROID_KEY_ALIAS:
|
||||||
|
required: true
|
||||||
|
ANDROID_KEY_PASSWORD:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
source_tag:
|
||||||
|
description: "Existing Android release to sign without rebuilding, for example android-v1.6.0-20261002."
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sign:
|
||||||
|
name: Sign and verify Android APK
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
SOURCE_TAG: ${{ inputs.source_tag }}
|
||||||
|
steps:
|
||||||
|
- name: Validate signing configuration
|
||||||
|
shell: python
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
import os
|
||||||
|
names = ("ANDROID_KEYSTORE_BASE64", "ANDROID_KEYSTORE_PASSWORD",
|
||||||
|
"ANDROID_KEY_ALIAS", "ANDROID_KEY_PASSWORD")
|
||||||
|
missing = [name for name in names if not os.environ.get(name)]
|
||||||
|
if missing:
|
||||||
|
raise ValueError("Missing repository signing secrets: " + ", ".join(missing))
|
||||||
|
|
||||||
|
- name: Resolve artifact version
|
||||||
|
id: version
|
||||||
|
shell: python
|
||||||
|
run: |
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
version = os.environ["VERSION"]
|
||||||
|
if os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch" and not version:
|
||||||
|
tag = os.environ["SOURCE_TAG"]
|
||||||
|
if not tag.startswith("android-v"):
|
||||||
|
raise ValueError("Use an existing android-v<app-version>-YYYYMMDD release")
|
||||||
|
version = tag.removeprefix("android-")
|
||||||
|
match = re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+-([0-9]{8})", version)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Invalid Android artifact version")
|
||||||
|
datetime.datetime.strptime(match[1], "%Y%m%d")
|
||||||
|
with open(os.environ["GITHUB_OUTPUT"], "a") as stream:
|
||||||
|
stream.write(f"version={version}\n")
|
||||||
|
|
||||||
|
- name: Download unsigned build artifact
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.version != ''
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: crossdesk-android-arm64-unsigned-${{ steps.version.outputs.version }}
|
||||||
|
path: unsigned
|
||||||
|
|
||||||
|
- name: Download unsigned APK from existing release
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.version == ''
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
APK_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
gh release view "${SOURCE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||||
|
--json isDraft,isPrerelease > release.json
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
release = json.loads(Path('release.json').read_text())
|
||||||
|
if release['isDraft'] or release['isPrerelease']:
|
||||||
|
raise ValueError('Use a published stable Android release')
|
||||||
|
PY
|
||||||
|
gh release download "${SOURCE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||||
|
--pattern "crossdesk-android-arm64-unsigned-${APK_VERSION}.apk" --dir unsigned
|
||||||
|
|
||||||
|
- name: Set up Java
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Android signing tools
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: "build-tools;36.0.0"
|
||||||
|
|
||||||
|
# This job never checks out or executes application source with the key present.
|
||||||
|
- name: Align, sign and verify Release APK
|
||||||
|
env:
|
||||||
|
APK_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
umask 077
|
||||||
|
crossdesk_signing_dir="$(mktemp -d "${RUNNER_TEMP}/crossdesk-signing.XXXXXX")"
|
||||||
|
trap 'rm -rf -- "${crossdesk_signing_dir}"' EXIT
|
||||||
|
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${crossdesk_signing_dir}/release.keystore"
|
||||||
|
crossdesk_tools="${ANDROID_HOME}/build-tools/36.0.0"
|
||||||
|
crossdesk_output="${RUNNER_TEMP}/crossdesk-android-signed"
|
||||||
|
mkdir -p "${crossdesk_output}"
|
||||||
|
"${crossdesk_tools}/zipalign" -P 16 -f -v 4 \
|
||||||
|
"unsigned/crossdesk-android-arm64-unsigned-${APK_VERSION}.apk" \
|
||||||
|
"${crossdesk_signing_dir}/aligned.apk"
|
||||||
|
"${crossdesk_tools}/apksigner" sign \
|
||||||
|
--ks "${crossdesk_signing_dir}/release.keystore" \
|
||||||
|
--ks-key-alias "${ANDROID_KEY_ALIAS}" \
|
||||||
|
--ks-pass env:ANDROID_KEYSTORE_PASSWORD --key-pass env:ANDROID_KEY_PASSWORD \
|
||||||
|
--out "${crossdesk_output}/crossdesk-android-arm64-${APK_VERSION}.apk" \
|
||||||
|
"${crossdesk_signing_dir}/aligned.apk"
|
||||||
|
"${crossdesk_tools}/apksigner" verify --verbose --print-certs \
|
||||||
|
"${crossdesk_output}/crossdesk-android-arm64-${APK_VERSION}.apk"
|
||||||
|
"${crossdesk_tools}/zipalign" -c -P 16 -v 4 \
|
||||||
|
"${crossdesk_output}/crossdesk-android-arm64-${APK_VERSION}.apk"
|
||||||
|
|
||||||
|
- name: Upload signed Release APK
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: crossdesk-android-arm64-${{ steps.version.outputs.version }}
|
||||||
|
path: ${{ runner.temp }}/crossdesk-android-signed/*.apk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Summarize signing
|
||||||
|
env:
|
||||||
|
APK_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
echo "Android ${APK_VERSION}: Release APK signed with the fixed publishing key; APK signature and 16 KB ZIP alignment verified." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -z "${VERSION}" ]]; then
|
||||||
|
echo "Signed an existing release without rebuilding. Download the signed Actions artifact to publish it." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
+36
-4
@@ -73,7 +73,7 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
|
|||||||
|
|
||||||
## GitHub Actions
|
## GitHub Actions
|
||||||
|
|
||||||
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建成功,并将未签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建及签名成功,并将已签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
||||||
|
|
||||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先提交 `app/build.gradle` 中需要发布的 `versionName`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先提交 `app/build.gradle` 中需要发布的 `versionName`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ git tag android-v1.6.0-20261002
|
|||||||
git push origin android-v1.6.0-20261002
|
git push origin android-v1.6.0-20261002
|
||||||
```
|
```
|
||||||
|
|
||||||
该标签只触发安卓构建,将未签名 Release APK 发布到独立的 GitHub Release 和现有下载服务器。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
该标签只触发安卓构建,使用固定发布密钥签名并验证 Release APK 后,再发布到独立的 GitHub Release 和现有下载服务器。签名失败或缺少签名 Secrets 时停止发布,不回退到未签名 APK。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
||||||
|
|
||||||
移动端独立发布保留桌面 `latest` 标签和 GitHub 的最新 Release 选择。APK 上传完成后,将服务器下载地址合并到共享 `https://version.crossdesk.cn/version.json` 的 `downloads.android-arm64`,并在该下载项中记录安卓版本和发布日期;保留顶层桌面版本信息和其他平台的下载项。下载服务器上传保留其他平台文件,复用仓库已有的 `SERVER_HOST`、`SERVER_USER`、`SERVER_KEY` Secrets。普通桌面版本标签仍通过 `Build and Release` 发布全部平台并更新各平台下载项;所有发布任务更新版本文件时使用同一队列。单独运行 `Build Android` 仍只上传 Actions 构建产物。
|
移动端独立发布保留桌面 `latest` 标签和 GitHub 的最新 Release 选择。APK 上传完成后,将服务器下载地址合并到共享 `https://version.crossdesk.cn/version.json` 的 `downloads.android-arm64`,并在该下载项中记录安卓版本和发布日期;保留顶层桌面版本信息和其他平台的下载项。下载服务器上传保留其他平台文件,复用仓库已有的 `SERVER_HOST`、`SERVER_USER`、`SERVER_KEY` Secrets。普通桌面版本标签仍通过 `Build and Release` 发布全部平台并更新各平台下载项;所有发布任务更新版本文件时使用同一队列。单独运行 `Build Android` 仍只上传 Actions 构建产物。
|
||||||
|
|
||||||
@@ -94,11 +94,43 @@ CI 构建 Debug、Release 和设备测试 APK,执行 Debug / Release Lint、
|
|||||||
|
|
||||||
产物使用 Android 工程自己的 `versionName`,例如 `v1.6.0-20261002`,不跟随桌面版本号。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
产物使用 Android 工程自己的 `versionName`,例如 `v1.6.0-20261002`,不跟随桌面版本号。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
||||||
|
|
||||||
- `crossdesk-android-arm64-unsigned-<版本>.apk`:未签名 Release APK,标签发布只收集此 APK,安装前需自行签名。
|
- `crossdesk-android-arm64-<版本>.apk`:已签名 Release APK,独立安卓发布和全平台标签发布收集此 APK,可直接安装。
|
||||||
|
- `crossdesk-android-arm64-unsigned-<版本>.apk`:构建工作流的中间产物,也可用于自行签名和验证修改后的构建。
|
||||||
- `crossdesk-android-arm64-debug-<版本>.apk` 和对应的 `-test.apk`:可安装的调试应用和设备测试包,保留 14 天。
|
- `crossdesk-android-arm64-debug-<版本>.apk` 和对应的 `-test.apk`:可安装的调试应用和设备测试包,保留 14 天。
|
||||||
- `crossdesk-android-reports`:Lint 报告,保留 14 天。
|
- `crossdesk-android-reports`:Lint 报告,保留 14 天。
|
||||||
|
|
||||||
当前 CI 不需要签名 Secrets。Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在发布新版本时递增 `app/build.gradle` 中的 `versionCode`。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在发布新版本时递增 `app/build.gradle` 中的 `versionCode`。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
||||||
|
|
||||||
|
### Release 签名
|
||||||
|
|
||||||
|
[`Sign Android Release`](../../.github/workflows/sign-android.yml) 在独立 job 中签名,不检出或执行应用源码。构建 job 和 Pull Request 构建不接收发布密钥。签名 job 使用 Build Tools 36.0.0,先执行 `zipalign -P 16`,再通过环境变量向 `apksigner` 提供密码,验证签名和 16 KB 对齐后上传已签名 APK。临时密钥库在步骤结束时清理,产物中不包含密钥或密码。
|
||||||
|
|
||||||
|
首次发布前,在仓库 Settings → Secrets and variables → Actions 配置:
|
||||||
|
|
||||||
|
| Secret | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ANDROID_KEYSTORE_BASE64` | 发布密钥库文件的 Base64 内容 |
|
||||||
|
| `ANDROID_KEYSTORE_PASSWORD` | 密钥库密码 |
|
||||||
|
| `ANDROID_KEY_ALIAS` | 发布私钥的 alias |
|
||||||
|
| `ANDROID_KEY_PASSWORD` | 发布私钥密码 |
|
||||||
|
|
||||||
|
APK 签名不需要 Google Play 开发者账号。若没有已有发布密钥,可使用 JDK 的 `keytool` 创建一次,密钥库保存在工程之外,并备份密钥和密码:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
keytool -genkeypair -storetype PKCS12 -keystore /path/outside/repo/crossdesk-release.p12 \
|
||||||
|
-alias crossdesk-release -keyalg RSA -keysize 4096 -validity 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
PKCS12 使用同一个密钥库和私钥密码,将它配置到两个密码 Secrets。可用 `gh secret set` 从标准输入写入 Secrets,避免把密码放进命令行参数。macOS 上传密钥库示例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
base64 -i /path/outside/repo/crossdesk-release.p12 | gh secret set ANDROID_KEYSTORE_BASE64 --repo kunkundi/crossdesk
|
||||||
|
gh secret set ANDROID_KEYSTORE_PASSWORD --repo kunkundi/crossdesk
|
||||||
|
gh secret set ANDROID_KEY_ALIAS --repo kunkundi/crossdesk
|
||||||
|
gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
|
||||||
|
```
|
||||||
|
|
||||||
|
后续版本沿用这些 Secrets,不能在每次构建时生成新密钥,也不能将密钥或密码提交到 Git。已有未签名安卓 Release 可单独运行 `Sign Android Release` 并填写其 `source_tag`,无需重新构建;该手动任务只上传已签名 Actions 产物,不修改已发布 Release。普通发布流程会自动签名、上传下载服务器,并将 `downloads.android-arm64` 指向已签名 APK。旧 Release 同时保留已签名和未签名 APK 时,版本文件优先选择相同版本的已签名文件。
|
||||||
|
|
||||||
## 使用与连接安全
|
## 使用与连接安全
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user