Skip to content

修复安卓端剧集详情页 404 问题:区分 series/media 类型路由 #22

修复安卓端剧集详情页 404 问题:区分 series/media 类型路由

修复安卓端剧集详情页 404 问题:区分 series/media 类型路由 #22

name: release-android
run-name: >-
release-android
${{ github.event_name == 'workflow_dispatch' && inputs.version_name || github.ref_name }}
on:
pull_request:
paths:
- 'android/**'
- 'scripts/android-version.sh'
- 'scripts/android-release-manifest.py'
- 'scripts/android-signing-preflight.sh'
- 'scripts/android-signing-bootstrap.sh'
- '.github/workflows/release-android.yml'
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version_name:
description: 'Android version name, for example 1.2.9 or 1.2.9-rc.1'
required: true
permissions:
contents: read
concurrency:
group: release-android-${{ github.ref }}
cancel-in-progress: false
env:
ANDROID_PRODUCTION_APPLICATION_ID: com.nowen.video
ANDROID_PRODUCTION_CERTIFICATE_FILE: android/signing/production-certificate.sha256
ANDROID_PRODUCTION_CERTIFICATE_PEM: android/signing/production-cert.pem
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 }}
ANDROID_APK: android/app/build/outputs/apk/release/app-release.apk
ANDROID_AAB: android/app/build/outputs/bundle/release/app-release.aab
jobs:
build:
name: Build signed Android
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
version_name: ${{ steps.version.outputs.version_name }}
version_code: ${{ steps.version.outputs.version_code }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle
- name: Resolve Android version and signing baseline
id: version
shell: bash
run: |
set -euo pipefail
bash -n scripts/android-signing-bootstrap.sh
bash scripts/android-version.sh --self-test
python3 scripts/android-release-manifest.py --self-test
bash scripts/android-signing-preflight.sh --self-test
production_certificate_sha256="$(tr -d ':[:space:]' < "$ANDROID_PRODUCTION_CERTIFICATE_FILE" | tr '[:upper:]' '[:lower:]')"
[[ "$production_certificate_sha256" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid production certificate SHA-256 baseline"; exit 1; }
public_certificate_sha256="$(openssl x509 -in "$ANDROID_PRODUCTION_CERTIFICATE_PEM" -outform DER | sha256sum | awk '{print $1}')"
[[ "$public_certificate_sha256" == "$production_certificate_sha256" ]] || {
echo "Production certificate PEM does not match SHA-256 baseline"
exit 1
}
echo "ANDROID_PRODUCTION_CERTIFICATE_SHA256=$production_certificate_sha256" >> "$GITHUB_ENV"
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
version_name="0.1.0-alpha.1"
elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
version_name="${GITHUB_REF_NAME#v}"
else
version_name='${{ inputs.version_name }}'
fi
version_code="$(bash scripts/android-version.sh "$version_name")"
{
echo "ANDROID_VERSION_NAME=$version_name"
echo "ANDROID_VERSION_CODE=$version_code"
} >> "$GITHUB_ENV"
echo "version_name=$version_name" >> "$GITHUB_OUTPUT"
echo "version_code=$version_code" >> "$GITHUB_OUTPUT"
- name: Prepare signing key
shell: bash
run: |
set -euo pipefail
store_file="$RUNNER_TEMP/nowen-video-android-release.jks"
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
export ANDROID_KEYSTORE_PASSWORD=android-ci
export ANDROID_KEY_ALIAS=android-ci
export ANDROID_KEY_PASSWORD=android-ci
keytool -genkeypair -noprompt \
-keystore "$store_file" \
-storepass "$ANDROID_KEYSTORE_PASSWORD" \
-keypass "$ANDROID_KEY_PASSWORD" \
-alias "$ANDROID_KEY_ALIAS" \
-keyalg RSA -keysize 2048 -validity 1 \
-dname "CN=Nowen Video Android CI, OU=CI, O=Nowen, L=CI, S=CI, C=US"
else
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
[[ -n "${!name:-}" ]] || { echo "Missing required Android signing secret: $name"; exit 1; }
done
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$store_file"
fi
chmod 600 "$store_file"
mkdir -p dist/android
preflight_args=(
--keystore "$store_file"
--alias "$ANDROID_KEY_ALIAS"
--version "$ANDROID_VERSION_NAME"
--repository "$GITHUB_REPOSITORY"
--report dist/android/signing-preflight.json
--skip-git-checks
)
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
preflight_args+=(--expected-fingerprint "$ANDROID_PRODUCTION_CERTIFICATE_SHA256")
fi
ANDROID_KEYSTORE_PASSWORD="$ANDROID_KEYSTORE_PASSWORD" \
ANDROID_KEY_PASSWORD="$ANDROID_KEY_PASSWORD" \
bash scripts/android-signing-preflight.sh "${preflight_args[@]}"
{
echo "ANDROID_SIGNING_STORE_FILE=$store_file"
echo "ANDROID_SIGNING_STORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD"
echo "ANDROID_SIGNING_KEY_ALIAS=$ANDROID_KEY_ALIAS"
echo "ANDROID_SIGNING_KEY_PASSWORD=$ANDROID_KEY_PASSWORD"
} >> "$GITHUB_ENV"
- name: Test and build signed APK/AAB
shell: bash
run: |
set -euo pipefail
chmod +x android/gradlew
./android/gradlew -p android \
clean testDebugUnitTest lintDebug assembleRelease bundleRelease \
--no-daemon --stacktrace
- name: Verify package identity and signatures
shell: bash
run: |
set -euo pipefail
[[ -f "$ANDROID_APK" && -f "$ANDROID_AAB" ]]
apksigner="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -n 1)"
aapt="$(find "$ANDROID_HOME/build-tools" -type f -name aapt | sort -V | tail -n 1)"
[[ -x "$apksigner" && -x "$aapt" ]]
"$apksigner" verify --verbose --print-certs "$ANDROID_APK" | tee "$RUNNER_TEMP/android-apksigner.txt"
jarsigner -verify "$ANDROID_AAB"
badging="$("$aapt" dump badging "$ANDROID_APK")"
application_id="$(printf '%s\n' "$badging" | sed -n "s/^package: name='\([^']*\)'.*/\1/p" | head -1)"
apk_version_code="$(printf '%s\n' "$badging" | sed -n "s/^package:.* versionCode='\([^']*\)'.*/\1/p" | head -1)"
apk_version_name="$(printf '%s\n' "$badging" | sed -n "s/^package:.* versionName='\([^']*\)'.*/\1/p" | head -1)"
min_sdk="$(printf '%s\n' "$badging" | sed -n "s/^sdkVersion:'\([^']*\)'.*/\1/p" | head -1)"
target_sdk="$(printf '%s\n' "$badging" | sed -n "s/^targetSdkVersion:'\([^']*\)'.*/\1/p" | head -1)"
[[ "$application_id" == "$ANDROID_PRODUCTION_APPLICATION_ID" ]] || { echo "Unexpected applicationId: $application_id"; exit 1; }
[[ "$apk_version_code" == "$ANDROID_VERSION_CODE" ]] || { echo "Unexpected versionCode: $apk_version_code"; exit 1; }
[[ "$apk_version_name" == "$ANDROID_VERSION_NAME" ]] || { echo "Unexpected versionName: $apk_version_name"; exit 1; }
certificate_sha256="$(sed -n 's/.*certificate SHA-256 digest: //Ip' "$RUNNER_TEMP/android-apksigner.txt" | head -1 | tr -d ':[:space:]' | tr '[:upper:]' '[:lower:]')"
[[ "$certificate_sha256" =~ ^[0-9a-f]{64}$ ]]
if [[ "$GITHUB_EVENT_NAME" != "pull_request" && "$certificate_sha256" != "$ANDROID_PRODUCTION_CERTIFICATE_SHA256" ]]; then
echo "Unexpected production signing certificate: $certificate_sha256"
exit 1
fi
{
echo "ANDROID_APPLICATION_ID=$application_id"
echo "ANDROID_APK_VERSION_CODE=$apk_version_code"
echo "ANDROID_APK_VERSION_NAME=$apk_version_name"
echo "ANDROID_MIN_SDK=$min_sdk"
echo "ANDROID_TARGET_SDK=$target_sdk"
echo "ANDROID_APK_CERTIFICATE_SHA256=$certificate_sha256"
} >> "$GITHUB_ENV"
- name: Generate release manifest, notes and checksums
shell: bash
run: |
set -euo pipefail
dist_apk="dist/android/nowen-video-android-${ANDROID_VERSION_NAME}.apk"
dist_aab="dist/android/nowen-video-android-${ANDROID_VERSION_NAME}.aab"
cp "$ANDROID_APK" "$dist_apk"
cp "$ANDROID_AAB" "$dist_aab"
python3 scripts/android-release-manifest.py \
--apk "$dist_apk" --aab "$dist_aab" \
--output dist/android/release-manifest.json \
--checksums-output dist/android/SHA256SUMS.txt \
--version-name "$ANDROID_VERSION_NAME" \
--version-code "$ANDROID_VERSION_CODE" \
--apk-version-name "$ANDROID_APK_VERSION_NAME" \
--apk-version-code "$ANDROID_APK_VERSION_CODE" \
--application-id "$ANDROID_APPLICATION_ID" \
--min-sdk "$ANDROID_MIN_SDK" --target-sdk "$ANDROID_TARGET_SDK" \
--certificate-sha256 "$ANDROID_APK_CERTIFICATE_SHA256"
python3 - <<'PY'
import json
from pathlib import Path
manifest = json.loads(Path("dist/android/release-manifest.json").read_text(encoding="utf-8"))
artifacts = {item["type"]: item for item in manifest["artifacts"]}
status = "这是预发布测试版本。" if manifest["channel"] != "stable" else "这是 Nowen Video Android 正式版本。"
replacements = {
"{{RELEASE_STATUS}}": status,
"{{VERSION_NAME}}": manifest["version"]["name"],
"{{VERSION_CODE}}": str(manifest["version"]["code"]),
"{{SOURCE_COMMIT}}": manifest["source"]["commit"],
"{{CERTIFICATE_SHA256}}": manifest["signing"]["certificate_sha256"],
"{{APK_SHA256}}": artifacts["apk"]["sha256"],
"{{AAB_SHA256}}": artifacts["aab"]["sha256"],
}
notes = Path("android/RELEASE_NOTES_TEMPLATE.md").read_text(encoding="utf-8")
for marker, value in replacements.items():
notes = notes.replace(marker, value)
if "{{" in notes:
raise SystemExit("Unresolved Android release notes placeholder")
Path("dist/android/RELEASE_NOTES.md").write_text(notes, encoding="utf-8")
PY
(cd dist/android && sha256sum -c SHA256SUMS.txt)
- name: Upload signed Android artifacts
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: nowen-video-android-release
path: dist/android/*
if-no-files-found: error
retention-days: 30
release:
name: Attach Android to product release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: nowen-video-android-release
path: artifacts
- name: Upload Android release assets
uses: softprops/action-gh-release@v2
with:
draft: true
prerelease: ${{ contains(needs.build.outputs.version_name, '-') }}
generate_release_notes: true
body_path: artifacts/RELEASE_NOTES.md
name: Nowen Video ${{ needs.build.outputs.version_name }}
files: artifacts/*