Skip to content

Merge pull request #94 from hoangkien1703/claude/project-thread-x0plw… #217

Merge pull request #94 from hoangkien1703/claude/project-thread-x0plw…

Merge pull request #94 from hoangkien1703/claude/project-thread-x0plw… #217

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"