Release #179
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 | |
| on: | |
| workflow_run: | |
| workflows: | |
| - "Bump" | |
| types: | |
| - completed | |
| permissions: | |
| contents: write | |
| env: | |
| SCHEME: GitOK | |
| CREATE_DMG_VERSION: "8.1.0" | |
| SPARKLE_VERSION: "2.8.1" | |
| R2_API_URL: https://data.coffic.cn | |
| jobs: | |
| build_with_signing: | |
| if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
| runs-on: macos-15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.workflow_run.head_branch }} | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: "latest-stable" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装依赖 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| curl -fsSL \ | |
| --header "authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "user-agent: GitOK-Release-Workflow" \ | |
| https://raw.githubusercontent.com/nookery/servon/main/install.sh | bash | |
| - name: 获取最新 tag | |
| run: | | |
| tag=$(git describe --tags --abbrev=0) | |
| echo "TAG=$tag" >> "$GITHUB_ENV" | |
| if [[ $tag == p* ]]; then | |
| echo "IS_PRERELEASE=true" >> "$GITHUB_ENV" | |
| previous_tag=$(git tag -l "p[0-9]*" | sort -V | grep -Fxv "$tag" | tail -n 1) | |
| if [ -z "$previous_tag" ]; then | |
| previous_tag=$(git tag -l "v*" | sort -V | tail -n 1) | |
| fi | |
| else | |
| echo "IS_PRERELEASE=false" >> "$GITHUB_ENV" | |
| previous_tag=$(git tag -l "v*" | sort -V | grep -Fxv "$tag" | tail -n 1) | |
| fi | |
| echo "PREVIOUS_TAG=$previous_tag" >> "$GITHUB_ENV" | |
| - name: Setup macOS signing | |
| id: signing | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} | |
| BUILD_CERTIFICATE_P12_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_P12_PASSWORD }} | |
| BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64_GITOK_MACOS }} | |
| APP_STORE_CONNECT_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }} | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| APP_STORE_CONNECT_KEY_ISSER_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ISSER_ID }} | |
| run: | | |
| source ./.github/scripts/setup-macos-signing.sh | |
| echo "signing_identity=$SIGNING_IDENTITY" >> "$GITHUB_OUTPUT" | |
| echo "api_key_path=$API_KEY_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Build and codesign | |
| run: | | |
| set -euo pipefail | |
| for arch in arm64 x86_64; do | |
| app="./temp/${arch}/Build/Products/Release/${{ env.SCHEME }}.app" | |
| servon xcode build --scheme ${{ env.SCHEME }} --arch "$arch" --clean=false -v | |
| servon xcode codesign --path "$app" \ | |
| --identity "${{ steps.signing.outputs.signing_identity }}" | |
| done | |
| - name: Create DMG | |
| env: | |
| DMG_SIGNING_IDENTITY: ${{ steps.signing.outputs.signing_identity }} | |
| run: | | |
| set -euo pipefail | |
| for arch in arm64 x86_64; do | |
| app="./temp/${arch}/Build/Products/Release/${{ env.SCHEME }}.app" | |
| out="./temp/dmg_${arch}" | |
| version_dmg=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" | tr . _) | |
| dmg_name="${{ env.SCHEME }}-${arch}-${version_dmg}.dmg" | |
| rm -rf "$out" | |
| mkdir -p "$out" | |
| set +e | |
| npx "create-dmg@${CREATE_DMG_VERSION}" --overwrite \ | |
| --identity "$DMG_SIGNING_IDENTITY" "$app" "$out" | |
| create_dmg_status=$? | |
| set -e | |
| generated_dmg="$(find "$out" -maxdepth 1 -name '*.dmg' -type f | head -n 1)" | |
| if [ -z "$generated_dmg" ]; then | |
| echo "❌ create-dmg did not produce a DMG for ${arch} (exit ${create_dmg_status})" >&2 | |
| exit 1 | |
| fi | |
| if [ "$create_dmg_status" -ne 0 ] && [ "$create_dmg_status" -ne 2 ]; then | |
| echo "❌ create-dmg failed for ${arch} (exit ${create_dmg_status})" >&2 | |
| exit "$create_dmg_status" | |
| fi | |
| mv "$generated_dmg" "./temp/${dmg_name}" | |
| rm -rf "$out" | |
| done | |
| ls -alh ./temp/*.dmg | |
| - name: Notarize DMG | |
| continue-on-error: true | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| for file in ./temp/*.dmg; do | |
| echo "🔐 Notarizing $file" | |
| xcrun notarytool submit "$file" \ | |
| --key ${{ steps.signing.outputs.api_key_path }} \ | |
| --key-id=${{ secrets.APP_STORE_CONNECT_KEY_ID }} \ | |
| --issuer ${{ secrets.APP_STORE_CONNECT_KEY_ISSER_ID }} \ | |
| --wait \ | |
| --timeout 10m | |
| stapler staple "$file" | |
| stapler validate "$file" | |
| done | |
| - name: Prepare updates | |
| run: | | |
| mkdir -p updates | |
| cp ./temp/*.dmg updates/ | |
| ls -alh updates | |
| - name: Generate Changelog | |
| run: | | |
| if [ -z "${PREVIOUS_TAG:-}" ]; then | |
| NOTES="## Release ${TAG}\n\nInitial release" | |
| else | |
| CHANGES=$(git log "${PREVIOUS_TAG}"..HEAD --no-merges --pretty=format:'- %s' \ | |
| | grep -Ev '^- (ci|chore|build|style|test)(\(|:)' \ | |
| | grep -Ev '^- ci:' \ | |
| | sort -u \ | |
| | head -20) | |
| if [ -z "$CHANGES" ]; then | |
| CHANGES="- Maintenance and stability improvements" | |
| fi | |
| NOTES="## Release ${TAG}\n\n### Changes since ${PREVIOUS_TAG}\n\n${CHANGES}" | |
| fi | |
| echo -e "$NOTES" > changelog.md | |
| cat changelog.md >> "$GITHUB_STEP_SUMMARY" | |
| - name: Generate appcasts | |
| if: env.IS_PRERELEASE == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| SPARKLE_BIN="$(SPARKLE_VERSION="${SPARKLE_VERSION}" .github/scripts/install-sparkle-tools.sh)" | |
| RELEASE_NOTES_HTML=$(gh api markdown -F text=@changelog.md -f mode=gfm) | |
| RELEASE_NOTES_LINK="https://github.com/CofficLab/GitOK/releases/latest" | |
| for arch in arm64 x86_64; do | |
| input_dir="appcast_input_${arch}" | |
| appcast_file="appcast-${arch}.xml" | |
| rm -rf "$input_dir" | |
| mkdir -p "$input_dir" | |
| cp ./updates/*"${arch}"*.dmg "$input_dir/" | |
| echo "🔑 Generating appcast for $arch..." | |
| echo "${{ secrets.SPARKLE_PRIVATE_KEY_GITOK }}" | \ | |
| "${SPARKLE_BIN}/generate_appcast" --ed-key-file - "$input_dir" | |
| if [ ! -f "$input_dir/appcast.xml" ]; then | |
| echo "❌ generate_appcast did not produce appcast.xml for $arch" | |
| ls -la "$input_dir" | |
| exit 1 | |
| fi | |
| cp "$input_dir/appcast.xml" "$appcast_file" | |
| dmg_file=$(basename "$(ls "$input_dir"/*.dmg | head -n 1)") | |
| export u="https://github.com/CofficLab/GitOK/releases/latest/download/$dmg_file" | |
| export RELEASE_NOTES_HTML | |
| export RELEASE_NOTES_LINK | |
| perl -i -pe 'if (!$done) { s/(enclosure url=")[^"]*/$1.$ENV{u}/e && ($done=1) }' "$appcast_file" | |
| perl -i -0777 -pe ' | |
| if (!$done++) { | |
| my $notes = $ENV{RELEASE_NOTES_HTML} // ""; | |
| my $link = $ENV{RELEASE_NOTES_LINK} // ""; | |
| my $insert = qq{\n <description><![CDATA[$notes]]></description>\n <sparkle:fullReleaseNotesLink>$link</sparkle:fullReleaseNotesLink>}; | |
| s/(<sparkle:minimumSystemVersion>[^<]+<\/sparkle:minimumSystemVersion>)(?!\s*<description)/$1$insert/s; | |
| } | |
| ' "$appcast_file" | |
| done | |
| cp appcast-arm64.xml appcast.xml | |
| - name: 发布 Release (prerelease) | |
| if: success() && env.IS_PRERELEASE == 'true' | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ env.TAG }} | |
| prerelease: true | |
| body_path: changelog.md | |
| files: ./updates/*.dmg | |
| - name: 发布 Release | |
| if: success() && env.IS_PRERELEASE == 'false' | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ env.TAG }} | |
| prerelease: false | |
| body_path: changelog.md | |
| files: | | |
| ./updates/*.dmg | |
| ./appcast.xml | |
| ./appcast-arm64.xml | |
| ./appcast-x86_64.xml | |
| - name: Upload to R2 | |
| if: success() | |
| env: | |
| WEBHOOK_SECRET: ${{ secrets.COFFIC_APP_UPLOAD_KEY }} | |
| run: | | |
| set -euo pipefail | |
| upload_file() { | |
| local file="$1" | |
| local key="$2" | |
| local response | |
| echo "📤 Uploading to R2: $key" | |
| response=$(curl -s -X POST "${R2_API_URL}/lumi/upload" \ | |
| -H "X-Webhook-Secret: ${WEBHOOK_SECRET}" \ | |
| -F "file=@${file}" \ | |
| -F "key=${key}") | |
| echo "Response: $response" | |
| echo "$response" | grep -q '"success":true' | |
| } | |
| shopt -s nullglob | |
| for dmg in ./updates/*.dmg; do | |
| upload_file "$dmg" "gitok/$(basename "$dmg")" || { | |
| echo "❌ Failed to upload $(basename "$dmg")" | |
| exit 1 | |
| } | |
| echo "✅ $(basename "$dmg") uploaded" | |
| done | |
| if [ "${IS_PRERELEASE}" = "false" ]; then | |
| for appcast in appcast.xml appcast-arm64.xml appcast-x86_64.xml; do | |
| if [ ! -f "$appcast" ]; then | |
| echo "⚠️ $appcast not found, skipping" | |
| continue | |
| fi | |
| if upload_file "$appcast" "gitok/${appcast}"; then | |
| echo "✅ $appcast uploaded" | |
| else | |
| echo "❌ Failed to upload $appcast" | |
| fi | |
| done | |
| fi |