Merge pull request #80 from hoangkien1703/claude/project-thread-p70bds #209
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 merged PR | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: Merged PR number to retry (uses its reserved version if one exists) | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: publish-main | |
| cancel-in-progress: false | |
| queue: max | |
| jobs: | |
| prepare: | |
| if: github.repository == 'hoangkien1703/dual-sub-replay' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| actions: read | |
| pull-requests: read | |
| issues: write # Create the optional release labels on first use. | |
| outputs: | |
| eligible: ${{ steps.plan.outputs.eligible }} | |
| release_needed: ${{ steps.plan.outputs.release_needed }} | |
| release_id: ${{ steps.plan.outputs.release_id }} | |
| source_sha: ${{ steps.plan.outputs.source_sha }} | |
| version: ${{ steps.plan.outputs.version }} | |
| version_code: ${{ steps.plan.outputs.version_code }} | |
| steps: | |
| - name: Check out trusted main workflow source and release history | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Ensure optional release labels exist | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| labels="$(gh label list --limit 1000 --json name --jq '.[].name')" | |
| for mode in patch minor major skip; do | |
| label="release:$mode" | |
| if ! grep -Fxq "$label" <<< "$labels"; then | |
| gh label create "$label" --color 1D76DB --description "Release automation: $mode" | |
| fi | |
| done | |
| - name: Verify merge and CI, then reserve or resume the release | |
| id: plan | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_PR: ${{ inputs.pr_number }} | |
| run: python3 tools/automatic_release.py prepare | |
| publish-main: | |
| needs: prepare | |
| if: needs.prepare.outputs.eligible == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Skip a release already published by an earlier attempt | |
| id: resume | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_ID: ${{ needs.prepare.outputs.release_id }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "$RELEASE_ID" ]]; then | |
| draft="$(gh api "repos/${GH_REPO}/releases/${RELEASE_ID}" --jq '.draft')" | |
| if [[ "$draft" == "false" ]]; then | |
| echo "build=false" >> "$GITHUB_OUTPUT" | |
| echo "Release is already public; skipping all builds and uploads." | |
| exit 0 | |
| fi | |
| [[ "$draft" == "true" ]] || exit 1 | |
| fi | |
| echo "build=true" >> "$GITHUB_OUTPUT" | |
| - name: Check out the exact reserved release source | |
| if: steps.resume.outputs.build == 'true' | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_sha }} | |
| persist-credentials: false | |
| - name: Set up Java 17 | |
| if: steps.resume.outputs.build == 'true' | |
| uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| - name: Set up Android SDK | |
| if: steps.resume.outputs.build == 'true' | |
| uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 | |
| with: | |
| packages: platform-tools | |
| - name: Install Android build SDK | |
| if: steps.resume.outputs.build == 'true' | |
| run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| - name: Set up Gradle cache | |
| if: steps.resume.outputs.build == 'true' | |
| uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4 | |
| - name: Restore persistent preview signing key | |
| if: steps.resume.outputs.build == 'true' | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.android/debug.keystore | |
| key: dual-sub-replay-preview-signing-v1 | |
| - name: Build main preview APK without repeating PR tests | |
| if: steps.resume.outputs.build == 'true' | |
| run: | | |
| set -euo pipefail | |
| preview_version_code=$(( $(date -u +%s) - 1700000000 )) | |
| bash ./gradlew assembleDebug \ | |
| -PpreviewVersionCode="$preview_version_code" \ | |
| -PpreviewVersionNameSuffix="-main-build${GITHUB_RUN_NUMBER}" \ | |
| -PpreviewApplicationIdSuffix=".preview" \ | |
| --stacktrace | |
| - name: Verify and package main preview APK | |
| if: steps.resume.outputs.build == 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release | |
| preview_apk="release/DualSub-Replay-preview.apk" | |
| cp app/build/outputs/apk/debug/app-debug.apk "$preview_apk" | |
| badging="$("$ANDROID_HOME/build-tools/36.0.0/aapt" dump badging "$preview_apk" | head -n 1)" | |
| echo "$badging" | |
| if [[ "$badging" != *"name='com.kienhoang.dualsubreplay.preview'"* ]]; then | |
| echo "Preview APK does not use the isolated preview application id" >&2 | |
| exit 1 | |
| fi | |
| sha256sum "$preview_apk" | |
| - name: Publish rolling main preview | |
| if: steps.resume.outputs.build == 'true' | |
| uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 | |
| with: | |
| tag_name: preview | |
| name: DualSub Replay preview | |
| prerelease: true | |
| make_latest: false | |
| body: | | |
| Testing build from a PR-verified merge into `main`. | |
| Most people should install the [latest official release](https://github.com/${{ github.repository }}/releases/latest) instead. | |
| Preview APKs install as **DualSub Replay Preview** with a separate Android package id, so they can coexist with the official app. | |
| files: release/DualSub-Replay-preview.apk | |
| overwrite_files: true | |
| - name: Restore production signing key | |
| if: steps.resume.outputs.build == 'true' && needs.prepare.outputs.release_needed == 'true' | |
| env: | |
| ANDROID_RELEASE_KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }} | |
| run: | | |
| if [[ -z "$ANDROID_RELEASE_KEYSTORE_BASE64" ]]; then | |
| echo "ANDROID_RELEASE_KEYSTORE_BASE64 is not configured" >&2 | |
| exit 1 | |
| fi | |
| printf '%s' "$ANDROID_RELEASE_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/dual-sub-replay-release.jks" | |
| chmod 600 "$RUNNER_TEMP/dual-sub-replay-release.jks" | |
| - name: Build production release APK | |
| if: steps.resume.outputs.build == 'true' && needs.prepare.outputs.release_needed == 'true' | |
| env: | |
| ANDROID_RELEASE_STORE_FILE: ${{ runner.temp }}/dual-sub-replay-release.jks | |
| ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} | |
| ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} | |
| ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} | |
| run: bash ./gradlew assembleRelease -PrequireReleaseSigning=true --stacktrace | |
| - name: Verify and package official APK | |
| if: steps.resume.outputs.build == 'true' && needs.prepare.outputs.release_needed == 'true' | |
| env: | |
| EXPECTED_VERSION: ${{ needs.prepare.outputs.version }} | |
| EXPECTED_CODE: ${{ needs.prepare.outputs.version_code }} | |
| run: | | |
| set -euo pipefail | |
| source_apk="app/build/outputs/apk/release/app-release.apk" | |
| release_apk="release/DualSub-Replay.apk" | |
| cp "$source_apk" "$release_apk" | |
| "$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --verbose --print-certs "$release_apk" | tee release/signing-report.txt | |
| if grep -q 'CN=Android Debug' release/signing-report.txt; then | |
| echo "Official APK was signed with the Android debug certificate" >&2 | |
| exit 1 | |
| fi | |
| badging="$("$ANDROID_HOME/build-tools/36.0.0/aapt" dump badging "$release_apk" | head -n 1)" | |
| echo "$badging" | |
| if [[ "$badging" != *"name='com.kienhoang.dualsubreplay'"* ]] || | |
| [[ "$badging" != *"versionName='$EXPECTED_VERSION'"* ]] || | |
| [[ "$badging" != *"versionCode='$EXPECTED_CODE'"* ]]; then | |
| echo "Official APK package/version differs from the release reservation" >&2 | |
| exit 1 | |
| fi | |
| sha256sum "$release_apk" | sed 's# release/# #' > release/DualSub-Replay.apk.sha256 | |
| - name: Upload verified APK and checksum to the reserved draft | |
| if: steps.resume.outputs.build == 'true' && needs.prepare.outputs.release_needed == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| run: >- | |
| gh release upload "v${RELEASE_VERSION}" | |
| release/DualSub-Replay.apk release/DualSub-Replay.apk.sha256 --clobber | |
| - name: Publish the complete official release | |
| if: steps.resume.outputs.build == 'true' && needs.prepare.outputs.release_needed == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ needs.prepare.outputs.release_id }} | |
| run: python3 tools/automatic_release.py publish --release-id "$RELEASE_ID" | |
| - name: Remove production signing material | |
| if: always() | |
| run: rm -f "$RUNNER_TEMP/dual-sub-replay-release.jks" |