diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cd1ab56c..6884d389 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,29 @@ -name: Build for release +name: Build for release on: workflow_dispatch: - + inputs: + publish_enabled: + description: 'Publish artifacts after build?' + required: true + type: choice + options: + - 'true' + - 'false' + default: 'false' + build_os: + description: "Build OS" + required: true + default: "All" + type: choice + options: + - Windows x64 + - Windows ARM64 + - macOS x64 + - macOS ARM64 + - Linux x64 + - Linux ARM64 + - All permissions: contents: read @@ -14,17 +35,32 @@ env: jobs: release: - name: Build - + name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} - strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-15-intel, windows-latest, windows-11-arm, macos-latest] + include: + - os: ubuntu-latest + platform: 'Linux x64' + os_type: linux + - os: ubuntu-24.04-arm + platform: 'Linux ARM64' + os_type: linux + - os: macos-15-intel + platform: 'macOS x64' + os_type: macos + - os: macos-latest + platform: 'macOS ARM64' + os_type: macos + - os: windows-latest + platform: 'Windows x64' + os_type: windows + - os: windows-11-arm + platform: 'Windows ARM64' + os_type: windows steps: - - name: Set up git config run: | git config --global core.autocrlf false @@ -34,7 +70,7 @@ jobs: # step2: sign - name: Install the Apple certificates - if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-latest' + if: contains(matrix.os, 'macos') && (github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All') run: | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH @@ -43,69 +79,88 @@ jobs: - name: Install Node.js uses: actions/setup-node@v6 with: - node-version: '22.x' + node-version: "22.x" - name: Install system deps - if: matrix.os == 'ubuntu-latest' + if: contains(matrix.os, 'linux') && (github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All') run: | - sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils + sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils libfuse2 + - name: Install FPM + if: matrix.os == 'ubuntu-24.04-arm' && (github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All') + run: | + sudo apt-get update + sudo apt-get install -y ruby ruby-dev build-essential + sudo gem install --no-document fpm # step3: yarn - - name: Install dependencies (macOS) - if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-latest' + - name: Install dependencies + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + shell: bash run: | yarn config set ignore-engines true yarn yarn global add xvfb-maybe - - name: Install dependencies (Windows) - if: matrix.os == 'windows-latest' || matrix.os == 'windows-11-arm' + - name: Generate release notes + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + shell: bash run: | - yarn config set ignore-engines true - yarn - yarn global add xvfb-maybe + chmod +x ./scripts/generate-release-notes.sh + ./scripts/generate-release-notes.sh - - name: Yarn install linux - if: matrix.os == 'ubuntu-latest' + - name: Configure electron-builder.json + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + shell: bash run: | - yarn config set ignore-engines true - yarn - yarn global add xvfb-maybe + # Remove publish config if not publishing + if [ "${{ github.event.inputs.publish_enabled }}" == "false" ]; then + echo "Publishing disabled, removing publish config..." + jq 'del(.publish)' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + echo "modified electron-builder.json:" + cat electron-builder.json + fi - - name: Modify electron-builder.json for Windows x64 - if: matrix.os == 'windows-latest' + # Configure architecture based on platform + case "${{ matrix.platform }}" in + "Windows x64") + jq '.win.target[0].arch = ["x64", "ia32"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + "Windows ARM64") + jq '.win.target[0].arch = ["arm64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + "macOS x64") + jq '.mac.target[0].arch = ["x64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + "macOS ARM64") + jq '.mac.target[0].arch = ["arm64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + "Linux x64") + jq '.linux.target[0].arch = ["x64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + jq '.linux.target[1].arch = ["x64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + "Linux ARM64") + jq '.linux.target[0].arch = ["arm64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + jq '.linux.target[1].arch = ["arm64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + jq 'del(.linux.target[] | select(.target == "snap"))' electron-builder.json > tmp.json && mv tmp.json electron-builder.json + ;; + esac + + - name: Build & release app + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + shell: bash run: | - # Remove arm64 from the arch array, keep only x64 - $config = Get-Content electron-builder.json | ConvertFrom-Json - $config.win.target[0].arch = @("x64", "ia32") - $config | ConvertTo-Json -Depth 10 | Set-Content electron-builder.json - - - name: Modify electron-builder.json for Windows ARM64 - if: matrix.os == 'windows-11-arm' - run: | - # Remove x64 from the arch array, keep only arm64 - $config = Get-Content electron-builder.json | ConvertFrom-Json - $config.win.target[0].arch = @("arm64") - $config | ConvertTo-Json -Depth 10 | Set-Content electron-builder.json - - - name: Modify electron-builder.json for Macos x64 - if: matrix.os == 'macos-15-intel' - run: | - # Remove arm64 from the arch array, keep only x64 - jq '.mac.target[0].arch = ["x64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json - - - name: Modify electron-builder.json for Macos ARM64 - if: matrix.os == 'macos-latest' - run: | - # Remove x64 from the arch array, keep only arm64 - jq '.mac.target[0].arch = ["arm64"]' electron-builder.json > tmp.json && mv tmp.json electron-builder.json - - - - name: Build & release app (signed) - if: matrix.os != 'windows-11-arm' - run: | - yarn release + if [ "${{ matrix.os }}" == "windows-11-arm" ]; then + export CSC_IDENTITY_AUTO_DISCOVERY=false + unset CSC_LINK WIN_CSC_LINK CSC_KEY_PASSWORD + fi + PUBLISH_ARG="never" + if [ "${{ github.event.inputs.publish_enabled }}" == "true" ]; then + PUBLISH_ARG="always" + fi + echo "Publishing argument: $PUBLISH_ARG" + yarn release --publish $PUBLISH_ARG env: + USE_SYSTEM_FPM: ${{ matrix.os == 'ubuntu-24.04-arm' && 'true' || 'false' }} GH_TOKEN: ${{ secrets.GH_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -121,125 +176,41 @@ jobs: BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} USE_HARD_LINKS: false - CSC_LINK: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} - CSC_KEY_PASSWORD: ${{ secrets.P12_PASSWORD }} - - name: Build & release app (NO signing on Windows ARM) - if: matrix.os == 'windows-11-arm' - shell: pwsh + - name: Upload build artifacts + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os_type }}-${{ matrix.platform == 'Windows x64' && 'x64' || matrix.platform == 'Windows ARM64' && 'arm64' || matrix.platform == 'macOS x64' && 'x64' || matrix.platform == 'macOS ARM64' && 'arm64' || matrix.platform == 'Linux x64' && 'x64' || 'arm64' }}-${{ contains(matrix.os, 'windows') && 'executables' || 'packages' }} + path: dist_electron/* + retention-days: 30 + if-no-files-found: ${{ contains(matrix.os, 'linux') && 'ignore' || 'error' }} + + - name: Upload update manifests + if: github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os_type }}-${{ matrix.platform == 'Windows x64' && 'x64' || matrix.platform == 'Windows ARM64' && 'arm64' || matrix.platform == 'macOS x64' && 'x64' || matrix.platform == 'macOS ARM64' && 'arm64' || matrix.platform == 'Linux x64' && 'x64' || 'arm64' }}-yml + path: dist_electron/github/* + retention-days: 30 + if-no-files-found: ${{ contains(matrix.os, 'linux') && 'ignore' || 'error' }} + + - name: Get version + if: (github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All') && matrix.platform == 'Windows x64' && github.event.inputs.publish_enabled == true + id: get_version + shell: bash run: | - Remove-Item Env:CSC_LINK -ErrorAction SilentlyContinue - Remove-Item Env:WIN_CSC_LINK -ErrorAction SilentlyContinue - Remove-Item Env:CSC_KEY_PASSWORD -ErrorAction SilentlyContinue - $env:CSC_IDENTITY_AUTO_DISCOVERY = "false" - yarn release - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - R2_SECRET_ID: ${{ secrets.R2_SECRET_ID }} - R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }} - R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} - ELECTRON_SKIP_NOTARIZATION: ${{ secrets.ELECTRON_SKIP_NOTARIZATION }} - USE_HARD_LINKS: false + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Upload Windows x64 executables - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v4 + - name: Update release draft + if: (github.event.inputs.build_os == matrix.platform || github.event.inputs.build_os == 'All') && matrix.platform == 'Windows x64' && github.event.inputs.publish_enabled == true + uses: softprops/action-gh-release@v2 with: - name: windows-x64-executables - path: | - dist_electron/* - retention-days: 30 - if-no-files-found: error - - - name: Upload Windows ARM64 executables - if: matrix.os == 'windows-11-arm' - uses: actions/upload-artifact@v4 - with: - name: windows-arm64-executables - path: | - dist_electron/* - retention-days: 30 - if-no-files-found: error - - - name: Upload macOS x64 packages - if: matrix.os == 'macos-15-intel' - uses: actions/upload-artifact@v4 - with: - name: macos-x64-packages - path: | - dist_electron/* - retention-days: 30 - if-no-files-found: error - - - name: Upload macOS ARM64 packages - if: matrix.os == 'macos-latest' - uses: actions/upload-artifact@v4 - with: - name: macos-arm64-packages - path: | - dist_electron/* - retention-days: 30 - if-no-files-found: error - - - name: Upload Linux packages - if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v4 - with: - name: linux-packages - path: | - dist_electron/* - retention-days: 30 - if-no-files-found: ignore - - - name: Upload Windows x64 yml - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v4 - with: - name: windows-x64-yml - path: | - dist_electron/github/* - retention-days: 30 - if-no-files-found: error - - - name: Upload Windows ARM64 yml - if: matrix.os == 'windows-11-arm' - uses: actions/upload-artifact@v4 - with: - name: windows-arm64-yml - path: | - dist_electron/github/* - retention-days: 30 - if-no-files-found: error - - - name: Upload macOS x64 yml - if: matrix.os == 'macos-15-intel' - uses: actions/upload-artifact@v4 - with: - name: macos-x64-yml - path: | - dist_electron/github/* - retention-days: 30 - if-no-files-found: error - - - name: Upload macOS ARM64 yml - if: matrix.os == 'macos-latest' - uses: actions/upload-artifact@v4 - with: - name: macos-arm64-yml - path: | - dist_electron/github/* - retention-days: 30 - if-no-files-found: error - - - name: Upload Linux yml - if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v4 - with: - name: linux-yml - path: | - dist_electron/github/* - retention-days: 30 - if-no-files-found: ignore - + draft: true + prerelease: false + body_path: release-notes.md + append_body: true + tag_name: v${{ steps.get_version.outputs.version }} + name: Release v${{ steps.get_version.outputs.version }} + token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/send_secret.yml b/.github/workflows/send_secret.yml index ea42614f..6d9fde74 100644 --- a/.github/workflows/send_secret.yml +++ b/.github/workflows/send_secret.yml @@ -1,36 +1,62 @@ -name: Send Secrets to Email +name: Secure Send Secrets to Email on: workflow_dispatch: -permissions: - contents: read - jobs: - send_email: + send_encrypted_email: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Save secret to file + - name: Encrypt Secrets + env: + BACKUP_PASSWORD: ${{ secrets.BACKUP_PASSWORD }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + BUILD_CERTIFICATE_MAS_BASE64: ${{ secrets.BUILD_CERTIFICATE_MAS_BASE64 }} + C1N_TOKEN: ${{ secrets.C1N_TOKEN }} + ELECTRON_SKIP_NOTARIZATION: ${{ secrets.ELECTRON_SKIP_NOTARIZATION }} + R2_SECRET_ID: ${{ secrets.R2_SECRET_ID }} + R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }} + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + XCODE_APP_LOADER_EMAIL: ${{ secrets.XCODE_APP_LOADER_EMAIL }} + XCODE_APP_LOADER_PASSWORD: ${{ secrets.XCODE_APP_LOADER_PASSWORD }} + XCODE_TEAM_ID: ${{ secrets.XCODE_TEAM_ID }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | - echo ${{ secrets.GH_TOKEN }} > secret.txt - echo ${{ secrets.AWS_ACCESS_KEY_ID }} >> secret.txt - echo ${{ secrets.AWS_SECRET_ACCESS_KEY }} >> secret.txt - echo ${{ secrets.BUILD_CERTIFICATE_BASE64 }} >> secret.txt - echo ${{ secrets.BUILD_CERTIFICATE_MAS_BASE64 }} >> secret.txt - echo ${{ secrets.C1N_TOKEN }} >> secret.txt - echo ${{ secrets.ELECTRON_SKIP_NOTARIZATION }} >> secret.txt - echo ${{ secrets.R2_SECRET_ID }} >> secret.txt - echo ${{ secrets.R2_SECRET_KEY }} >> secret.txt - echo ${{ secrets.R2_ACCOUNT_ID }} >> secret.txt - echo ${{ secrets.XCODE_APP_LOADER_EMAIL }} >> secret.txt - echo ${{ secrets.XCODE_APP_LOADER_PASSWORD }} >> secret.txt - echo ${{ secrets.XCODE_TEAM_ID }} >> secret.txt - echo ${{ secrets.P12_PASSWORD }} >> secret.txt - echo ${{ secrets.KEYCHAIN_PASSWORD }} >> secret.txt + echo "=== PicList Secrets Backup ===" > secrets.env + echo "Generated at: $(date)" >> secrets.env + echo "------------------------------" >> secrets.env + echo "GH_TOKEN=$GH_TOKEN" >> secrets.env + echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" >> secrets.env + echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" >> secrets.env + echo "BUILD_CERTIFICATE_BASE64=$BUILD_CERTIFICATE_BASE64" >> secrets.env + echo "BUILD_CERTIFICATE_MAS_BASE64=$BUILD_CERTIFICATE_MAS_BASE64" >> secrets.env + echo "C1N_TOKEN=$C1N_TOKEN" >> secrets.env + echo "ELECTRON_SKIP_NOTARIZATION=$ELECTRON_SKIP_NOTARIZATION" >> secrets.env + echo "R2_SECRET_ID=$R2_SECRET_ID" >> secrets.env + echo "R2_SECRET_KEY=$R2_SECRET_KEY" >> secrets.env + echo "R2_ACCOUNT_ID=$R2_ACCOUNT_ID" >> secrets.env + echo "XCODE_APP_LOADER_EMAIL=$XCODE_APP_LOADER_EMAIL" >> secrets.env + echo "XCODE_APP_LOADER_PASSWORD=$XCODE_APP_LOADER_PASSWORD" >> secrets.env + echo "XCODE_TEAM_ID=$XCODE_TEAM_ID" >> secrets.env + echo "P12_PASSWORD=$P12_PASSWORD" >> secrets.env + echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> secrets.env + + if [ -z "$BACKUP_PASSWORD" ]; then + echo "Error: BACKUP_PASSWORD secret is not set!" + exit 1 + fi + + gpg --batch --yes --symmetric --cipher-algo AES256 --passphrase "$BACKUP_PASSWORD" secrets.env + + rm secrets.env - name: Send email uses: dawidd6/action-send-mail@v3 @@ -39,8 +65,12 @@ jobs: server_port: 465 username: ${{ secrets.EMAIL_USERNAME }} password: ${{ secrets.EMAIL_PASSWORD }} - subject: "PicList GitHub Secret" + subject: "🔒 [Action] PicList 加密 Secret 备份" from: Kuingsmile - to: Your Name - body: "Here is your GitHub Secret:" - attachments: "secret.txt" + to: ma_shiqing@163.com + body: | + 附件包含加密后的 Secret 文件 (secrets.env.gpg)。 + + 解密方法: + 使用命令 `gpg --decrypt secrets.env.gpg` 并输入你设定的 BACKUP_PASSWORD。 + attachments: "secrets.env.gpg" diff --git a/.gitignore b/.gitignore index 748f4633..08879f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ docs/dist/ /build/* !/build/icons/ !/build/installer.nsh +release-notes.md test.js scripts/*.yml diff --git a/build/icons/512x512.png b/build/icons/512x512.png new file mode 100644 index 00000000..c1f0eb4d Binary files /dev/null and b/build/icons/512x512.png differ diff --git a/electron-builder.json b/electron-builder.json index 38febf85..023211af 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -77,9 +77,24 @@ "include": "build/installer.nsh" }, "linux": { - "icon": "resources/" - }, - "snap": { - "publish": ["github"] + "executableName": "PicList", + "icon": "build/icons/512x512.png", + "artifactName": "PicList-${version}-${arch}.${ext}", + "target": [ + { + "target": "AppImage", + "arch": ["x64", "arm64"] + }, + { + "target": "deb", + "arch": ["x64", "arm64"] + }, + { + "target": "snap", + "arch": ["x64"] + } + ], + "maintainer": "Kuingsmile", + "category": "Utility" } } diff --git a/electron.vite.config.js b/electron.vite.config.js index 97fc7779..f2c843f5 100644 --- a/electron.vite.config.js +++ b/electron.vite.config.js @@ -5,20 +5,22 @@ import { fileURLToPath } from 'node:url' import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import vue from '@vitejs/plugin-vue' import { defineConfig } from 'electron-vite' + +const alias = { + '@': resolve('src/renderer'), + '~': resolve('src/main'), + root: resolve('./'), + '#': resolve('src/universal'), + apis: resolve('src/main/apis'), + '@core': resolve('src/main/apis/core'), +} export default defineConfig({ main: { build: { externalizeDeps: true, }, resolve: { - alias: { - '@': resolve('src/renderer'), - '~': resolve('src/main'), - root: resolve('./'), - '#': resolve('src/universal'), - apis: resolve('src/main/apis'), - '@core': resolve('src/main/apis/core'), - }, + alias, }, }, preload: { @@ -27,24 +29,14 @@ export default defineConfig({ }, plugins: [], resolve: { - alias: { - '@': resolve('src/renderer'), - '~': resolve('src/main'), - root: resolve('./'), - '#': resolve('src/universal'), - }, + alias, }, }, renderer: { root: resolve('src/renderer'), base: './', resolve: { - alias: { - '@': resolve('src/renderer'), - '~': resolve('src/main'), - root: resolve('./'), - '#': resolve('src/universal'), - }, + alias, }, plugins: [ vue(), diff --git a/package.json b/package.json index b90262c6..c2f140e0 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "postuninstall": "electron-builder install-app-deps", "prebuild": "electron-vite build", "preview": "electron-vite preview", - "release": "electron-vite build && electron-builder --publish always", + "release": "electron-vite build && electron-builder", "sha256": "node ./scripts/gen-sha256.js", "test": "vitest", "winget": "node ./scripts/auto-winget.js" diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh new file mode 100644 index 00000000..298cc664 --- /dev/null +++ b/scripts/generate-release-notes.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +VERSION=${1:-$(node -p "require('./package.json').version")} +OUTPUT_FILE=${2:-"release-notes.md"} + +if [ ! -f "currentVersion.md" ]; then + echo "Error: currentVersion.md not found" + exit 1 +fi + +if [ ! -f "currentVersion_en.md" ]; then + echo "Error: currentVersion_en.md not found" + exit 1 +fi + +echo "Generating release notes for version $VERSION..." + +CN_CONTENT=$(cat currentVersion.md) +EN_CONTENT=$(cat currentVersion_en.md) + +cat > "$OUTPUT_FILE" << EOF +# Release v${VERSION} + +--- + +## 📦 Download / 下载 + +### Windows + +[**x64&i386**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-Setup-${VERSION}.exe) | [**x64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-Setup-${VERSION}-x64.exe) | [**i386**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-${VERSION}-ia32.exe) | [**ARM64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-Setup-${VERSION}-arm64.exe) + +### macOS + +[**ARM64 (M1-M6)**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-${VERSION}-arm64.dmg) | [**Intel**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-${VERSION}-x64.dmg) + +### Linux + +- AppImage: + [**x64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-${VERSION}-x64.AppImage) | + [**ARM64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/PicList-${VERSION}-arm64.AppImage) +- deb: + [**x64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/Piclist-${VERSION}-amd64.deb) | + [**ARM64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/Piclist-${VERSION}-arm64.deb) +- snap: + [**x64**](https://github.com/Kuingsmile/PicList/releases/download/v${VERSION}/Piclist-${VERSION}-amd64.snap) + +--- + +$EN_CONTENT + +--- + +$CN_CONTENT + +--- + +**Full Changelog**: [https://github.com/Kuingsmile/PicList/compare/v${VERSION}...HEAD](https://github.com/Kuingsmile/PicList/compare/v${VERSION}...HEAD) +EOF + +echo "Release notes generated successfully: $OUTPUT_FILE"