From 47268dea6782bc616f99908d587ea7a4e1027b98 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 8 Mar 2026 22:10:59 +0500 Subject: [PATCH] ci: add TestFlight release pipeline and workflow improvements --- .gitea/workflows/ci-scraper.yaml | 4 + .gitea/workflows/ci-ui.yaml | 4 + .gitea/workflows/deploy.yaml | 20 ++-- .gitea/workflows/ios.yaml | 161 ++++++++++++++++++++----------- .gitea/workflows/release-ui.yaml | 4 + ios/LibNovel/ExportOptions.plist | 21 ++++ justfile | 17 ++++ 7 files changed, 164 insertions(+), 67 deletions(-) create mode 100644 ios/LibNovel/ExportOptions.plist diff --git a/.gitea/workflows/ci-scraper.yaml b/.gitea/workflows/ci-scraper.yaml index 396fd19..768b778 100644 --- a/.gitea/workflows/ci-scraper.yaml +++ b/.gitea/workflows/ci-scraper.yaml @@ -14,6 +14,10 @@ on: - "justfile" - ".gitea/workflows/ci-scraper.yaml" +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + jobs: # ── lint & vet ─────────────────────────────────────────────────────────────── lint: diff --git a/.gitea/workflows/ci-ui.yaml b/.gitea/workflows/ci-ui.yaml index be38d7c..d58b643 100644 --- a/.gitea/workflows/ci-ui.yaml +++ b/.gitea/workflows/ci-ui.yaml @@ -14,6 +14,10 @@ on: - "justfile" - ".gitea/workflows/ci-ui.yaml" +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + jobs: # ── type-check & build ─────────────────────────────────────────────────────── build: diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 91ad599..38861bd 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -19,14 +19,18 @@ on: # Header: x-api-key: # Response on success: HTTP 200, body: [{"result":{"data":{"json":{...}}}}] +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + jobs: # ── production deploy (main/master only) ───────────────────────────────────── deploy-production: name: Deploy Production runs-on: ubuntu-latest if: > - github.event_name == 'push' && - (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + gitea.event_name == 'push' && + (gitea.ref == 'refs/heads/main' || gitea.ref == 'refs/heads/master') steps: - name: Redeploy production stack run: | @@ -46,15 +50,15 @@ jobs: name: Deploy Preview runs-on: ubuntu-latest if: > - github.event_name == 'push' && - github.ref != 'refs/heads/main' && - github.ref != 'refs/heads/master' + gitea.event_name == 'push' && + gitea.ref != 'refs/heads/main' && + gitea.ref != 'refs/heads/master' steps: - name: Sanitize branch name id: branch run: | # Lowercase, replace non-alphanumeric with dashes, strip trailing dashes, max 20 chars - SUFFIX=$(echo "${{ github.ref_name }}" \ + SUFFIX=$(echo "${{ gitea.ref_name }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g' \ | cut -c1-20 \ @@ -81,12 +85,12 @@ jobs: cleanup-preview: name: Cleanup Preview runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.action == 'closed' + if: gitea.event_name == 'pull_request' && gitea.event.action == 'closed' steps: - name: Sanitize branch name id: branch run: | - SUFFIX=$(echo "${{ github.head_ref }}" \ + SUFFIX=$(echo "${{ gitea.head_ref }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g' \ | cut -c1-20 \ diff --git a/.gitea/workflows/ios.yaml b/.gitea/workflows/ios.yaml index 0db523c..8a68a12 100644 --- a/.gitea/workflows/ios.yaml +++ b/.gitea/workflows/ios.yaml @@ -7,6 +7,9 @@ on: - "ios/**" - "justfile" - ".gitea/workflows/ios.yaml" + push_tag: + tags: + - "ios-v*" pull_request: branches: ["v2", "main"] paths: @@ -14,38 +17,38 @@ on: - "justfile" - ".gitea/workflows/ios.yaml" +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + jobs: - # ── build ───────────────────────────────────────────────────────────────── + # ── build (simulator) ───────────────────────────────────────────────────── build: name: Build runs-on: macos-latest + if: ${{ !startsWith(gitea.ref, 'refs/tags/ios-v') }} steps: - uses: actions/checkout@v4 - name: Install just - run: brew install just - - - name: Install xcodegen - run: brew install xcodegen + run: npm install -g rust-just - name: Build (simulator) run: just ios-build - # ── test ────────────────────────────────────────────────────────────────── + # ── unit tests ──────────────────────────────────────────────────────────── test: name: Test runs-on: macos-latest needs: build + if: ${{ !startsWith(gitea.ref, 'refs/tags/ios-v') }} steps: - uses: actions/checkout@v4 - name: Install just - run: brew install just - - - name: Install xcodegen - run: brew install xcodegen + run: npm install -g rust-just - name: Run unit tests run: just ios-test @@ -58,52 +61,92 @@ jobs: path: ios/LibNovel/test-results.xml retention-days: 7 - # ── archive (release IPA) ───────────────────────────────────────────────── - # Runs only on pushes to v2/main (not PRs). - # Requires secrets: APPLE_CERTIFICATE_BASE64, APPLE_CERTIFICATE_PASSWORD, - # APPLE_PROVISIONING_PROFILE_BASE64, KEYCHAIN_PASSWORD - # Also requires ios/LibNovel/ExportOptions.plist to exist. - # - # archive: - # name: Archive - # runs-on: macos-latest - # needs: [build, test] - # if: gitea.event_name == 'push' - # - # steps: - # - uses: actions/checkout@v4 - # - # - name: Install just - # run: brew install just - # - # - name: Install xcodegen - # run: brew install xcodegen - # - # - name: Import signing certificate - # env: - # CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - # CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - # run: | - # KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - # security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - # security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - # security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - # echo "$CERTIFICATE_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12 - # security import $RUNNER_TEMP/cert.p12 \ - # -P "$CERTIFICATE_PASSWORD" \ - # -A -t cert -f pkcs12 \ - # -k $KEYCHAIN_PATH - # security list-keychain -d user -s $KEYCHAIN_PATH - # - # - name: Import provisioning profile - # env: - # PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }} - # run: | - # PP_PATH=$RUNNER_TEMP/profile.mobileprovision - # echo "$PROFILE_BASE64" | base64 --decode > $PP_PATH - # mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles - # cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/ - # - # - name: Archive & export IPA - # run: just ios-archive && just ios-export + # ── archive & release to TestFlight ────────────────────────────────────── + # Triggered only on ios-v* tags (e.g. ios-v1.0.0). + # Required secrets: + # APPLE_CERTIFICATE_BASE64 - Distribution certificate (.p12) base64-encoded + # APPLE_CERTIFICATE_PASSWORD - Password for the .p12 file + # APPLE_PROVISIONING_PROFILE_BASE64 - App Store distribution profile base64-encoded + # KEYCHAIN_PASSWORD - Temporary keychain password (any random string) + # ASC_KEY_ID - App Store Connect API key ID + # ASC_ISSUER_ID - App Store Connect issuer ID + # ASC_PRIVATE_KEY - Contents of the .p8 private key file + # APPLE_TEAM_ID - 10-character Apple Developer team ID (GHZXC6FVMU) + release: + name: Release to TestFlight + runs-on: macos-latest + if: ${{ startsWith(gitea.ref, 'refs/tags/ios-v') }} + + steps: + - uses: actions/checkout@v4 + + - name: Install just + run: npm install -g rust-just + + - name: Set build number from run number + run: just ios-set-build-number ${{ gitea.run_number }} + + - name: Import signing certificate + env: + CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + echo "$CERTIFICATE_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 \ + -P "$CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + - name: Import provisioning profile + env: + PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }} + run: | + PP_PATH=$RUNNER_TEMP/profile.mobileprovision + echo "$PROFILE_BASE64" | base64 --decode > $PP_PATH + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/ + + - name: Write App Store Connect API key + env: + ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + run: | + mkdir -p ~/private_keys + echo "$ASC_PRIVATE_KEY" > ~/private_keys/AuthKey_$ASC_KEY_ID.p8 + + - name: Inject team ID into ExportOptions.plist + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + /usr/libexec/PlistBuddy -c \ + "Set :teamID $APPLE_TEAM_ID" \ + ios/LibNovel/ExportOptions.plist + + - name: Archive + run: just ios-archive + + - name: Export IPA + run: just ios-export + + - name: Upload to TestFlight + env: + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + run: just ios-upload + + - name: Upload IPA artifact + uses: actions/upload-artifact@v4 + with: + name: LibNovel-${{ gitea.ref_name }}.ipa + path: ${{ env.RUNNER_TEMP }}/ipa/LibNovel.ipa + retention-days: 30 + + - name: Cleanup keychain + if: always() + run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db diff --git a/.gitea/workflows/release-ui.yaml b/.gitea/workflows/release-ui.yaml index 0feabe0..2b564d7 100644 --- a/.gitea/workflows/release-ui.yaml +++ b/.gitea/workflows/release-ui.yaml @@ -5,6 +5,10 @@ on: tags: - "v*" +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + defaults: run: working-directory: ui diff --git a/ios/LibNovel/ExportOptions.plist b/ios/LibNovel/ExportOptions.plist new file mode 100644 index 0000000..03ed4e4 --- /dev/null +++ b/ios/LibNovel/ExportOptions.plist @@ -0,0 +1,21 @@ + + + + + method + app-store-connect + teamID + $(DEVELOPMENT_TEAM) + uploadBitcode + + uploadSymbols + + signingStyle + manual + provisioningProfiles + + cc.kalekber.libnovel + LibNovel Distribution + + + diff --git a/justfile b/justfile index 55d78ad..6258fc0 100644 --- a/justfile +++ b/justfile @@ -147,6 +147,23 @@ ios-export: -exportPath {{runner_temp}}/ipa \ -exportOptionsPlist ExportOptions.plist +# Set the build number (CFBundleVersion) in project.yml before archiving. +# Usage: just ios-set-build-number 42 +ios-set-build-number number: + cd {{ios_dir}} && sed -i '' \ + 's/CURRENT_PROJECT_VERSION: .*/CURRENT_PROJECT_VERSION: {{number}}/' \ + project.yml + +# Upload the exported IPA to TestFlight via App Store Connect API. +# Requires env vars: ASC_KEY_ID, ASC_ISSUER_ID, ASC_PRIVATE_KEY_PATH +# The private key (.p8 file) must be present at ASC_PRIVATE_KEY_PATH. +ios-upload: + xcrun altool --upload-app \ + --type ios \ + --file {{runner_temp}}/ipa/LibNovel.ipa \ + --apiKey "$ASC_KEY_ID" \ + --apiIssuer "$ASC_ISSUER_ID" + # ─── Docker Compose ─────────────────────────────────────────────────────────── # Start all services (browserless, kokoro, scraper, minio, pocketbase)