修复安卓端剧集详情页 404 问题:区分 series/media 类型路由 #22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/* |