Production Release Pipeline #139
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: "Production Release Pipeline" | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version_name: | |
| description: 'Version Name – semver X.Y.Z (e.g. 1.2.3)' | |
| required: true | |
| version_code: | |
| description: 'Version Code override (integer). Leave blank to auto‑calculate.' | |
| required: false | |
| default: '' | |
| release_notes: | |
| description: 'Release notes (Markdown)' | |
| required: false | |
| default: '' | |
| draft: | |
| description: 'Create release as a draft?' | |
| required: false | |
| default: false | |
| type: boolean | |
| prerelease: | |
| description: 'Mark as a pre‑release?' | |
| required: false | |
| default: false | |
| type: boolean | |
| build_android: | |
| description: 'Build Android APK + Root Module ZIP' | |
| required: false | |
| default: true | |
| type: boolean | |
| build_windows: | |
| description: 'Build PC Tool – Windows format' | |
| required: false | |
| default: 'both' | |
| type: choice | |
| options: | |
| - both | |
| - exe | |
| - msi | |
| - none | |
| build_linux: | |
| description: 'Build PC Tool – Linux (.deb)' | |
| required: false | |
| default: true | |
| type: boolean | |
| build_macos: | |
| description: 'Build PC Tool – macOS (.dmg)' | |
| required: false | |
| default: true | |
| type: boolean | |
| sign_apk: | |
| description: 'Sign the APK (requires keystore secrets)' | |
| required: false | |
| default: true | |
| type: boolean | |
| force_version: | |
| description: 'Override – skip version code regression check (use to re‑deploy)' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| pages: write | |
| issues: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ============================================================ | |
| # 1. VALIDATE VERSION & GENERATE NOTES | |
| # ============================================================ | |
| validate: | |
| name: "Validate Version & Generate Notes" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version_name: ${{ steps.resolve-version.outputs.VERSION_NAME }} | |
| version_code: ${{ steps.resolve-version.outputs.VERSION_CODE }} | |
| tag_name: ${{ steps.resolve-version.outputs.TAG_NAME }} | |
| release_notes: ${{ steps.generate-notes.outputs.BODY }} | |
| started_at: ${{ steps.resolve-version.outputs.STARTED_AT }} | |
| steps: | |
| - name: "Checkout repository (full history for changelog)" | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: "Resolve & Validate Version Number" | |
| id: resolve-version | |
| shell: pwsh | |
| run: | | |
| $version_name = "${{ github.event.inputs.version_name }}" | |
| $version_code_raw = "${{ github.event.inputs.version_code }}" | |
| # Tag‑triggered run: extract version from the tag itself | |
| if ("${{ github.event_name }}" -eq "push") { | |
| $version_name = "${{ github.ref_name }}".TrimStart('v') | |
| } | |
| if ([string]::IsNullOrWhiteSpace($version_name)) { | |
| throw "FATAL: version_name is required." | |
| } | |
| if ($version_name -notmatch '^\d+\.\d+\.\d+$') { | |
| throw "FATAL: '$version_name' is not a valid semver (X.Y.Z)." | |
| } | |
| $parts = $version_name -split '\.' | |
| $major = [int]$parts[0] | |
| $minor = [int]$parts[1] | |
| $patch = [int]$parts[2] | |
| if (-not [string]::IsNullOrWhiteSpace($version_code_raw)) { | |
| $version_code = [int]$version_code_raw | |
| } else { | |
| $version_code = ($major * 10000) + ($minor * 100) + $patch | |
| } | |
| $started_at = (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC") | |
| echo "VERSION_NAME=$version_name" >> $env:GITHUB_OUTPUT | |
| echo "VERSION_CODE=$version_code" >> $env:GITHUB_OUTPUT | |
| echo "TAG_NAME=v$version_name" >> $env:GITHUB_OUTPUT | |
| echo "STARTED_AT=$started_at" >> $env:GITHUB_OUTPUT | |
| - name: "Guard Against Duplicate Releases" | |
| shell: bash | |
| run: | | |
| FORCE="${{ github.event.inputs.force_version }}" | |
| if [ "$FORCE" = "true" ]; then | |
| echo "force_version=true — skipping duplicate release guard." | |
| exit 0 | |
| fi | |
| TAG="${{ steps.resolve-version.outputs.TAG_NAME }}" | |
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG") | |
| if [ "$STATUS" = "200" ]; then | |
| echo "::error::A release for $TAG already exists. Aborting." | |
| exit 1 | |
| fi | |
| - name: "Check Version Code Regression" | |
| if: github.event.inputs.force_version != 'true' | |
| shell: bash | |
| run: | | |
| NEW_CODE=${{ steps.resolve-version.outputs.VERSION_CODE }} | |
| PAGES_URL="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/update.json" | |
| if curl -sf "$PAGES_URL" -o live_update.json; then | |
| LIVE_CODE=$(jq -r '.versionCode' live_update.json) | |
| if [ "$NEW_CODE" -le "$LIVE_CODE" ]; then | |
| echo "::error::Version code regression: new=$NEW_CODE <= live=$LIVE_CODE" | |
| exit 1 | |
| fi | |
| else | |
| echo "No existing update.json found – regression check skipped." | |
| fi | |
| - name: "Generate Release Notes" | |
| id: generate-notes | |
| shell: pwsh | |
| run: | | |
| $v = "${{ steps.resolve-version.outputs.VERSION_NAME }}" | |
| $c = "${{ steps.resolve-version.outputs.VERSION_CODE }}" | |
| $manual = "${{ github.event.inputs.release_notes }}" | |
| $body = "" | |
| if (-not [string]::IsNullOrWhiteSpace($manual)) { | |
| $body = $manual | |
| } else { | |
| $prev = git describe --tags --abbrev=0 HEAD^ 2>$null | |
| if ($prev) { | |
| # Group commits by conventional-commit type for readability | |
| $lines = @() | |
| $currentType = "" | |
| git log "$prev..HEAD" --pretty=format:"%s" --no-merges | ForEach-Object { | |
| $msg = $_ | |
| if ($msg -match '^(feat|fix|docs|refactor|chore|ci|test|style|perf|build|revert)(\([^)]+\))?:\s*(.*)') { | |
| $type = $matches[1] | |
| $scope = $matches[2] | |
| $desc = $matches[3] | |
| if ($type -ne $currentType) { | |
| $currentType = $type | |
| $typeLabel = @{ | |
| 'feat' = '🚀 Features' | |
| 'fix' = '🐛 Bug Fixes' | |
| 'docs' = '📚 Documentation' | |
| 'refactor' = '♻️ Refactoring' | |
| 'chore' = '🧹 Chores' | |
| 'ci' = '⚙️ CI/CD' | |
| 'test' = '🧪 Tests' | |
| 'style' = '💄 Styling' | |
| 'perf' = '⚡ Performance' | |
| 'build' = '🏗️ Build' | |
| 'revert' = '⏪ Reverts' | |
| } | |
| $lines += "`n### $($typeLabel[$type])" | |
| } | |
| $lines += "- $desc $scope" | |
| } else { | |
| $lines += "- $msg" | |
| } | |
| } | |
| if ($lines.Count -gt 0) { | |
| $body += "## What's Changed (since $prev)`n" | |
| $body += ($lines -join "`n") | |
| } else { | |
| $body += "## What's Changed`n" | |
| $body += "- Maintenance and stability improvements." | |
| } | |
| } else { | |
| $log = git log --pretty=format:"- %s" --no-merges -30 | |
| $body += "## Initial Release`n$log" | |
| } | |
| } | |
| $body += "`n`n___`n### 📥 Requirements`n| | |`n|---|---|`n| **Root** | Magisk / KernelSU / APatch / SuperSU |`n| **Framework** | LSPosed (recommended) – or Overlay Mode (no framework) |`n| **Android** | 6.0+ (API 23) |`n| **Version Code** | $c |`n`n### 🆘 Support`n- **Issues**: https://github.com/${{ github.repository }}/issues`n- **Security**: https://github.com/${{ github.repository }}/security/advisories`n- **Documentation**: https://github.com/${{ github.repository }}#readme" | |
| echo "BODY<<EOF" >> $env:GITHUB_OUTPUT | |
| echo $body >> $env:GITHUB_OUTPUT | |
| echo "EOF" >> $env:GITHUB_OUTPUT | |
| # ============================================================ | |
| # 2. RUN UNIT TESTS | |
| # ============================================================ | |
| test: | |
| name: "Unit Tests" | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "Checkout Code" | |
| uses: actions/checkout@v7 | |
| - name: "Set up Java 17" | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: "Make Gradlew Executable" | |
| run: chmod +x gradlew | |
| - name: "Execute Unit Tests" | |
| run: ./gradlew test -PVERSION_NAME="${{ needs.validate.outputs.version_name }}" -PVERSION_CODE=${{ needs.validate.outputs.version_code }} | |
| - name: "Upload Test Reports" | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-reports | |
| path: '**/build/reports/tests/' | |
| retention-days: 7 | |
| # ============================================================ | |
| # 3. BUILD PC (Windows / Linux / macOS matrix) | |
| # ============================================================ | |
| build-pc: | |
| name: "Build PC – ${{ matrix.label }}" | |
| needs: [validate, test] | |
| if: | | |
| always() && | |
| needs.validate.result == 'success' && | |
| needs.test.result == 'success' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| label: Windows | |
| artifact_name: pc-tool-windows | |
| file_glob: "*.exe" | |
| java_version: '17' | |
| - os: ubuntu-latest | |
| label: Linux | |
| artifact_name: pc-tool-linux | |
| file_glob: "*.deb" | |
| java_version: '17' | |
| - os: macos-latest | |
| label: macOS | |
| artifact_name: pc-tool-macos | |
| file_glob: "*.dmg" | |
| java_version: '21' | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: "Checkout Code" | |
| uses: actions/checkout@v7 | |
| - name: "Set up Java ${{ matrix.java_version }}" | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: ${{ matrix.java_version }} | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: "Make Gradlew Executable" | |
| run: chmod +x gradlew | |
| - name: "Export winFormat for Gradle" | |
| id: win-format | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| echo "WIN_FORMAT=both" >> $GITHUB_OUTPUT | |
| elif [ "${{ matrix.label }}" = "Windows" ]; then | |
| echo "WIN_FORMAT=${{ github.event.inputs.build_windows }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "WIN_FORMAT=both" >> $GITHUB_OUTPUT | |
| fi | |
| - name: "Build ${{ matrix.label }} Distribution" | |
| id: build-pc | |
| if: | | |
| github.event_name == 'push' || | |
| (matrix.label == 'Windows' && github.event.inputs.build_windows != 'none') || | |
| (matrix.label == 'Linux' && github.event.inputs.build_linux == 'true') || | |
| (matrix.label == 'macOS' && github.event.inputs.build_macos == 'true') | |
| shell: bash | |
| run: | | |
| ./gradlew buildPC \ | |
| -PVERSION_NAME="${{ needs.validate.outputs.version_name }}" \ | |
| -PVERSION_CODE=${{ needs.validate.outputs.version_code }} \ | |
| -PwinFormat=${{ steps.win-format.outputs.WIN_FORMAT }} | |
| - name: "Stage ${{ matrix.label }} Artifact" | |
| if: steps.build-pc.outcome == 'success' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path staging -Force | |
| $winFormat = "${{ steps.win-format.outputs.WIN_FORMAT }}" | |
| $fileGlob = "${{ matrix.file_glob }}" | |
| # Skip *.exe glob when building msi-only (no .exe produced) | |
| if (($fileGlob -eq "*.exe") -and ($winFormat -eq "msi")) { | |
| Write-Host "Skipping *.exe glob (msi-only Windows build)" -ForegroundColor Yellow | |
| } else { | |
| $files = @(Get-ChildItem -Path "pc-tool-kotlin/build/compose/binaries" -Recurse -Filter $fileGlob) | |
| if ($files.Count -eq 0) { throw "No file matching $fileGlob found in pc-tool-kotlin/build/compose/binaries" } | |
| foreach ($f in $files) { Copy-Item -Path $f.FullName -Destination staging/ } | |
| } | |
| # On Windows we may also have a .msi (needed for exe-only or dual-format) | |
| if ("${{ matrix.label }}" -eq "Windows" -and $winFormat -ne "exe") { | |
| $msiFiles = @(Get-ChildItem -Path "pc-tool-kotlin/build/compose/binaries" -Recurse -Filter "*.msi" -ErrorAction SilentlyContinue) | |
| foreach ($f in $msiFiles) { Copy-Item -Path $f.FullName -Destination staging/ } | |
| } | |
| - name: "Upload ${{ matrix.label }} Artifact" | |
| if: steps.build-pc.outcome == 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: staging/* | |
| # ============================================================ | |
| # 4. BUILD ANDROID APK & MODULE | |
| # ============================================================ | |
| build-android: | |
| name: "Build Android APK & Module" | |
| needs: [validate, test] | |
| if: | | |
| always() && | |
| needs.validate.result == 'success' && | |
| needs.test.result == 'success' && | |
| (github.event_name == 'push' || github.event.inputs.build_android == 'true') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "Checkout Code" | |
| uses: actions/checkout@v7 | |
| - name: "Set up Java 17" | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: "Make Gradlew Executable" | |
| run: chmod +x gradlew | |
| - name: "Resolve signing config" | |
| id: signing-config | |
| shell: bash | |
| run: | | |
| DO_SIGN="false" | |
| if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event.inputs.sign_apk }}" = "true" ]; then | |
| DO_SIGN="true" | |
| fi | |
| if [ "$DO_SIGN" = "true" ]; then | |
| if [ -z "${{ secrets.SIGNING_KEY }}" ]; then | |
| echo "::error::APK signing enabled but SIGNING_KEY secret is missing" | |
| exit 1 | |
| fi | |
| echo "${{ secrets.SIGNING_KEY }}" | base64 -d > release.keystore | |
| echo "SIGN_APK=true" >> $GITHUB_ENV | |
| echo "KEYSTORE_FILE=release.keystore" >> $GITHUB_ENV | |
| else | |
| echo "SIGN_APK=false" >> $GITHUB_ENV | |
| fi | |
| - name: "Build Android APK & Root Module (unsigned)" | |
| shell: bash | |
| run: | | |
| ./gradlew buildAndroid buildModule \ | |
| -PVERSION_NAME="${{ needs.validate.outputs.version_name }}" \ | |
| -PVERSION_CODE=${{ needs.validate.outputs.version_code }} \ | |
| -PSIGN_APK=false | |
| - name: "Sign APK with apksigner" | |
| if: env.SIGN_APK == 'true' | |
| shell: bash | |
| run: | | |
| BUILD_TOOLS=$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1) | |
| UNSIGNED="android-app/app/build/outputs/apk/release/app-release-unsigned.apk" | |
| SIGNED="android-app/app/build/outputs/apk/release/app-release.apk" | |
| "$ANDROID_HOME/build-tools/$BUILD_TOOLS/apksigner" sign \ | |
| --ks release.keystore \ | |
| --ks-pass "pass:${{ secrets.KEY_STORE_PASSWORD }}" \ | |
| --ks-key-alias "${{ secrets.ALIAS }}" \ | |
| --key-pass "pass:${{ secrets.KEY_PASSWORD }}" \ | |
| --out "$SIGNED" \ | |
| "$UNSIGNED" | |
| - name: "Stage Android Artifacts" | |
| shell: bash | |
| run: | | |
| mkdir -p android-staging | |
| APK="android-app/app/build/outputs/apk/release/app-release.apk" | |
| if [ ! -f "$APK" ]; then | |
| APK="android-app/app/build/outputs/apk/release/app-release-unsigned.apk" | |
| fi | |
| if [ ! -f "$APK" ]; then | |
| echo "::error::No APK found in android-app/app/build/outputs/apk/release/" | |
| ls -la android-app/app/build/outputs/apk/release/ 2>/dev/null || echo "(directory may be empty)" | |
| exit 1 | |
| fi | |
| cp "$APK" android-staging/app-release.apk | |
| cp build/distributions/InputBlockerModule.zip android-staging/ | |
| - name: "Upload Android Artifacts" | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: android-artifacts | |
| path: android-staging/* | |
| # ============================================================ | |
| # 5. PUBLISH RELEASE & DEPLOY update.json | |
| # ============================================================ | |
| release: | |
| name: "Publish Release & Deploy update.json" | |
| needs: [validate, build-pc, build-android] | |
| if: | | |
| always() && | |
| needs.validate.result == 'success' && | |
| !contains(needs.*.result, 'failure') && | |
| !contains(needs.*.result, 'cancelled') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "Checkout Code" | |
| uses: actions/checkout@v7 | |
| - name: "Download All Build Artifacts" | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: raw-artifacts | |
| - name: "Organise & Rename Artifacts for Release" | |
| shell: pwsh | |
| run: | | |
| $v = "${{ needs.validate.outputs.version_name }}" | |
| $do_android = "${{ github.event.inputs.build_android }}" -ne 'false' | |
| $do_windows = "${{ github.event.inputs.build_windows }}" -ne 'none' | |
| $do_linux = "${{ github.event.inputs.build_linux }}" -ne 'false' | |
| $do_macos = "${{ github.event.inputs.build_macos }}" -ne 'false' | |
| if ("${{ github.event_name }}" -eq "push") { | |
| $do_android = $true; $do_windows = $true; $do_linux = $true; $do_macos = $true | |
| } | |
| New-Item -ItemType Directory -Path "release-assets" -Force | |
| if ($do_android) { | |
| Copy-Item "raw-artifacts/android-artifacts/app-release.apk" "release-assets/InputBlocker-v$v.apk" | |
| Copy-Item "raw-artifacts/android-artifacts/InputBlockerModule.zip" "release-assets/InputBlocker-v$v.zip" | |
| } | |
| if ($do_windows) { | |
| $win_format = "${{ github.event.inputs.build_windows }}" | |
| if ("${{ github.event_name }}" -eq "push") { $win_format = "both" } | |
| if ($win_format -eq 'both' -or $win_format -eq 'exe') { | |
| $exe = Get-ChildItem "raw-artifacts/pc-tool-windows/*.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($exe) { Copy-Item $exe.FullName "release-assets/InputBlockerSetup-v$v.exe" } | |
| } | |
| if ($win_format -eq 'both' -or $win_format -eq 'msi') { | |
| $msi = Get-ChildItem "raw-artifacts/pc-tool-windows/*.msi" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($msi) { Copy-Item $msi.FullName "release-assets/InputBlockerSetup-v$v.msi" } | |
| } | |
| } | |
| if ($do_linux) { | |
| $app = Get-ChildItem "raw-artifacts/pc-tool-linux/*.deb" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($app) { Copy-Item $app.FullName "release-assets/InputBlockerSetup-v$v.deb" } | |
| } | |
| if ($do_macos) { | |
| $dmg = Get-ChildItem "raw-artifacts/pc-tool-macos/*.dmg" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($dmg) { Copy-Item $dmg.FullName "release-assets/InputBlockerSetup-v$v.dmg" } | |
| } | |
| - name: "Verify Mandatory Artifacts Present" | |
| shell: pwsh | |
| run: | | |
| $v = "${{ needs.validate.outputs.version_name }}" | |
| $do_android = "${{ github.event.inputs.build_android }}" -ne 'false' | |
| $do_windows = "${{ github.event.inputs.build_windows }}" -ne 'none' | |
| $is_push = "${{ github.event_name }}" -eq "push" | |
| if ($is_push) { $do_windows = $true } | |
| $required = @() | |
| if ($do_android -or $is_push) { | |
| $required += "release-assets/InputBlocker-v$v.apk" | |
| $required += "release-assets/InputBlocker-v$v.zip" | |
| } | |
| if ($do_windows) { | |
| $win_format = "${{ github.event.inputs.build_windows }}" | |
| if ($is_push) { $win_format = "both" } | |
| if ($win_format -eq 'both' -or $win_format -eq 'exe') { $required += "release-assets/InputBlockerSetup-v$v.exe" } | |
| if ($win_format -eq 'both' -or $win_format -eq 'msi') { $required += "release-assets/InputBlockerSetup-v$v.msi" } | |
| } | |
| if ($required.Count -eq 0) { | |
| throw "No artifacts selected for build – nothing to release." | |
| } | |
| foreach ($file in $required) { | |
| if (-not (Test-Path $file)) { | |
| throw "Missing required artifact: $file" | |
| } | |
| } | |
| - name: "Verify APK Signature" | |
| if: | | |
| (github.event_name == 'push' || github.event.inputs.sign_apk == 'true') && | |
| (github.event_name == 'push' || github.event.inputs.build_android == 'true') | |
| shell: bash | |
| run: | | |
| APK="release-assets/InputBlocker-v${{ needs.validate.outputs.version_name }}.apk" | |
| if [ ! -f "$APK" ]; then echo "No APK to verify, skipping."; exit 0; fi | |
| BUILD_TOOLS=$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1) | |
| "$ANDROID_HOME/build-tools/$BUILD_TOOLS/apksigner" verify "$APK" | |
| - name: "Verify ZIP Integrity" | |
| if: github.event_name == 'push' || github.event.inputs.build_android == 'true' | |
| shell: bash | |
| run: | | |
| unzip -t "release-assets/InputBlocker-v${{ needs.validate.outputs.version_name }}.zip" | |
| - name: "Generate SHA-256 Checksums" | |
| shell: bash | |
| run: | | |
| cd release-assets | |
| sha256sum * > "checksums-v${{ needs.validate.outputs.version_name }}.txt" | |
| - name: "Upload Final Release Assets (for inspection)" | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-assets-v${{ needs.validate.outputs.version_name }} | |
| path: release-assets/* | |
| - name: "Create GitHub Release" | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.validate.outputs.tag_name }} | |
| name: "Release ${{ needs.validate.outputs.tag_name }}" | |
| body: ${{ needs.validate.outputs.release_notes }} | |
| draft: ${{ github.event.inputs.draft || 'false' }} | |
| prerelease: ${{ github.event.inputs.prerelease || 'false' }} | |
| files: release-assets/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: "Deploy update.json for in‑app updater" | |
| if: github.event_name == 'push' || github.event.inputs.build_android == 'true' | |
| shell: bash | |
| run: | | |
| V="${{ needs.validate.outputs.version_name }}" | |
| C="${{ needs.validate.outputs.version_code }}" | |
| REPO="${{ github.repository }}" | |
| APK_SHA=$(sha256sum "release-assets/InputBlocker-v$V.apk" | awk '{print $1}') | |
| ZIP_SHA=$(sha256sum "release-assets/InputBlocker-v$V.zip" | awk '{print $1}') | |
| mkdir -p pages-deploy | |
| # Write changelog to a temp file so jq can read it verbatim | |
| echo "${{ needs.validate.outputs.release_notes }}" > /tmp/changelog.md | |
| jq -n \ | |
| --arg version "$V" \ | |
| --argjson versionCode "$C" \ | |
| --arg apkUrl "https://github.com/$REPO/releases/download/v$V/InputBlocker-v$V.apk" \ | |
| --arg zipUrl "https://github.com/$REPO/releases/download/v$V/InputBlocker-v$V.zip" \ | |
| --rawfile changelog /tmp/changelog.md \ | |
| --arg apkSha256 "$APK_SHA" \ | |
| --arg zipSha256 "$ZIP_SHA" \ | |
| '{ | |
| "version": $version, | |
| "versionCode": $versionCode, | |
| "apkUrl": $apkUrl, | |
| "zipUrl": $zipUrl, | |
| "changelog": $changelog, | |
| "apkSha256": $apkSha256, | |
| "zipSha256": $zipSha256 | |
| }' > pages-deploy/update.json | |
| - name: "Publish update.json via GitHub Pages" | |
| if: github.event_name == 'push' || github.event.inputs.build_android == 'true' | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./pages-deploy | |
| keep_files: true | |
| commit_message: "Deploy update.json for v${{ needs.validate.outputs.version_name }}" | |
| - name: "Create Bug Report on Pipeline Failure" | |
| if: failure() | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: "Release pipeline failed – v${{ needs.validate.outputs.version_name }}", | |
| body: `Pipeline run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}`, | |
| labels: ["bug", "release"] | |
| }) |