Skip to content

Release

Release #179

Workflow file for this run

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