From 74f04427af0da775b72542d8fada53f07bcb3ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=A8=E8=8B=92?= <16112591+chen-ran@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:58:23 +0800 Subject: [PATCH] chore(ci): unify release pipeline --- .github/workflows/cli-release.yml | 154 ---------------- .github/workflows/release.yml | 266 ++++++++++++++++++++++++++++ .github/workflows/tauri-release.yml | 152 ---------------- 3 files changed, 266 insertions(+), 306 deletions(-) delete mode 100644 .github/workflows/cli-release.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/tauri-release.yml diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml deleted file mode 100644 index 4226096a..00000000 --- a/.github/workflows/cli-release.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: CLI Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - inputs: - version: - description: "Release version, e.g. 1.2.3 or v1.2.3" - required: false - publish: - description: "Create or update the GitHub release" - required: false - default: "true" - -permissions: - contents: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name || github.run_id }} - cancel-in-progress: false - -jobs: - build: - name: Build ${{ matrix.goos }}/${{ matrix.goarch }} - runs-on: ubuntu-latest - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - include: - - goos: linux - goarch: amd64 - - goos: linux - goarch: arm64 - - goos: darwin - goarch: amd64 - - goos: darwin - goarch: arm64 - - goos: windows - goarch: amd64 - - goos: windows - goarch: arm64 - - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: pnpm - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - cache: true - - - name: Resolve release metadata - id: meta - shell: bash - run: | - set -euo pipefail - RAW_VERSION="${{ github.event.inputs.version }}" - if [[ "${{ github.event_name }}" == "workflow_dispatch" && -z "$RAW_VERSION" ]]; then - echo "workflow_dispatch requires the version input" >&2 - exit 1 - fi - if [[ -z "$RAW_VERSION" ]]; then - RAW_VERSION="${GITHUB_REF_NAME}" - fi - VERSION="${RAW_VERSION#v}" - TAG_NAME="${RAW_VERSION}" - if [[ "$TAG_NAME" != v* ]]; then - TAG_NAME="v${TAG_NAME}" - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - - - name: Install JS dependencies - run: pnpm install --frozen-lockfile - - - name: Build release archive - shell: bash - env: - VERSION: ${{ steps.meta.outputs.version }} - COMMIT_HASH: ${{ github.sha }} - TARGET_OS: ${{ matrix.goos }} - TARGET_ARCH: ${{ matrix.goarch }} - run: | - set -euo pipefail - bash scripts/release.sh \ - --version "$VERSION" \ - --commit-hash "$COMMIT_HASH" \ - --os "$TARGET_OS" \ - --arch "$TARGET_ARCH" \ - --output-dir dist - - - name: Upload workflow artifacts - uses: actions/upload-artifact@v4 - with: - name: memoh-${{ matrix.goos }}-${{ matrix.goarch }} - path: | - dist/*.tar.gz - dist/*.zip - if-no-files-found: error - - release: - name: Publish GitHub release - runs-on: ubuntu-latest - needs: build - if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true' - steps: - - name: Resolve release metadata - id: meta - shell: bash - run: | - set -euo pipefail - RAW_VERSION="${{ github.event.inputs.version }}" - if [[ "${{ github.event_name }}" == "workflow_dispatch" && -z "$RAW_VERSION" ]]; then - echo "workflow_dispatch requires the version input" >&2 - exit 1 - fi - if [[ -z "$RAW_VERSION" ]]; then - RAW_VERSION="${GITHUB_REF_NAME}" - fi - VERSION="${RAW_VERSION#v}" - TAG_NAME="${RAW_VERSION}" - if [[ "$TAG_NAME" != v* ]]; then - TAG_NAME="v${TAG_NAME}" - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - - - name: Download release artifacts - uses: actions/download-artifact@v4 - with: - path: release-artifacts - pattern: memoh-* - merge-multiple: true - - - name: Publish release assets - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.meta.outputs.tag_name }} - name: Memoh ${{ steps.meta.outputs.tag_name }} - generate_release_notes: true - files: | - release-artifacts/*.tar.gz - release-artifacts/*.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..24b9ed59 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,266 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + changelog: + name: Generate changelog + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + registry-url: https://registry.npmjs.org/ + + - run: pnpm dlx changelogithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cli-build: + name: Build CLI ${{ matrix.goos }}/${{ matrix.goarch }} + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: pnpm + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Install JS dependencies + run: pnpm install --frozen-lockfile + - name: Build release archive + shell: bash + env: + COMMIT_HASH: ${{ github.sha }} + TARGET_OS: ${{ matrix.goos }} + TARGET_ARCH: ${{ matrix.goarch }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + bash scripts/release.sh \ + --version "$VERSION" \ + --commit-hash "$COMMIT_HASH" \ + --os "$TARGET_OS" \ + --arch "$TARGET_ARCH" \ + --output-dir dist + - name: Upload CLI artifacts + uses: actions/upload-artifact@v4 + with: + name: cli-${{ matrix.goos }}-${{ matrix.goarch }} + path: | + dist/*.tar.gz + dist/*.zip + if-no-files-found: error + + cli-upload: + name: Upload CLI artifacts + runs-on: ubuntu-latest + needs: [changelog, cli-build] + steps: + - name: Download CLI artifacts + uses: actions/download-artifact@v4 + with: + path: release-artifacts/cli + pattern: cli-* + merge-multiple: true + - name: Publish CLI assets + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + gh release upload "$TAG_NAME" release-artifacts/cli/*.tar.gz release-artifacts/cli/*.zip --clobber + + desktop-build: + name: Build desktop ${{ matrix.target }} + runs-on: ${{ matrix.platform }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + target: aarch64-apple-darwin + - platform: macos-latest + target: x86_64-apple-darwin + - platform: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + - platform: windows-latest + target: x86_64-pc-windows-msvc + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + steps: + - uses: actions/checkout@v4 + - name: Sync desktop version files + shell: bash + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + export VERSION + node <<'EOF' + const fs = require('fs'); + const version = process.env.VERSION; + const packageJsonPath = 'apps/desktop/package.json'; + const tauriConfPath = 'apps/desktop/src-tauri/tauri.conf.json'; + const cargoTomlPath = 'apps/desktop/src-tauri/Cargo.toml'; + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.version = version; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + + const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8')); + tauriConf.version = version; + fs.writeFileSync(tauriConfPath, `${JSON.stringify(tauriConf, null, 2)}\n`); + + const cargoToml = fs + .readFileSync(cargoTomlPath, 'utf8') + .replace(/^version = ".*"$/m, `version = "${version}"`); + fs.writeFileSync(cargoTomlPath, cargoToml); + EOF + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: pnpm + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: apps/desktop/src-tauri -> target + - name: Install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf + - name: Install JS dependencies + run: pnpm install --frozen-lockfile + - name: Prepare macOS code signing + if: ${{ matrix.platform == 'macos-latest' && env.APPLE_CERTIFICATE != '' && env.APPLE_CERTIFICATE_PASSWORD != '' }} + shell: bash + run: | + set -euo pipefail + KEYCHAIN_PASSWORD="github-actions-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}" + python3 - <<'EOF' + import base64 + import os + from pathlib import Path + + target = Path(os.environ["RUNNER_TEMP"]) / "certificate.p12" + target.write_bytes(base64.b64decode(os.environ["APPLE_CERTIFICATE"])) + EOF + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import "$RUNNER_TEMP/certificate.p12" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + curl -fsSL -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer + security add-certificates -k build.keychain "$RUNNER_TEMP/DeveloperIDG2CA.cer" + security find-identity -v -p codesigning build.keychain + IDENTITY=$(security find-identity -v -p codesigning build.keychain | awk -F'"' '/Developer ID Application/ { print $2; exit }') + if [[ -z "$IDENTITY" ]]; then + echo "No Developer ID Application identity in build keychain" + exit 1 + fi + echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" + - name: Build desktop bundles + shell: bash + run: | + set -euo pipefail + ARGS=(build --target "${{ matrix.target }}") + if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then + VERSION="${GITHUB_REF_NAME#v}" + NUMERIC_VERSION="${VERSION%%-*}" + NUMERIC_VERSION="${NUMERIC_VERSION%%+*}" + ARGS+=(--config "{\"bundle\":{\"windows\":{\"wix\":{\"version\":\"${NUMERIC_VERSION}\"}}}}") + fi + pnpm --filter @memohai/desktop tauri "${ARGS[@]}" + - name: Upload desktop artifacts + uses: actions/upload-artifact@v4 + with: + name: desktop-${{ matrix.target }} + path: | + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.app.tar.gz + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.deb + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.dmg + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.msi + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.rpm + apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.sig + if-no-files-found: error + + desktop-upload: + name: Upload desktop artifacts + runs-on: ubuntu-latest + needs: [changelog, desktop-build] + steps: + - name: Download desktop artifacts + uses: actions/download-artifact@v4 + with: + path: release-artifacts/desktop + pattern: desktop-* + merge-multiple: true + - name: Publish desktop assets + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + mapfile -t files < <(rg --files release-artifacts/desktop -g '*.AppImage' -g '*.app.tar.gz' -g '*.deb' -g '*.dmg' -g '*.exe' -g '*.msi' -g '*.rpm' -g '*.sig') + if [[ ${#files[@]} -eq 0 ]]; then + echo "No desktop artifacts found" >&2 + exit 1 + fi + gh release upload "$TAG_NAME" "${files[@]}" --clobber diff --git a/.github/workflows/tauri-release.yml b/.github/workflows/tauri-release.yml deleted file mode 100644 index 235bbd97..00000000 --- a/.github/workflows/tauri-release.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: Tauri Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-release: - name: Release (${{ matrix.platform }}) - strategy: - fail-fast: false - matrix: - include: - - platform: macos-latest - target: aarch64-apple-darwin - - platform: macos-latest - target: x86_64-apple-darwin - - platform: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - - platform: windows-latest - target: x86_64-pc-windows-msvc - - runs-on: ${{ matrix.platform }} - timeout-minutes: 60 - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - steps: - - uses: actions/checkout@v4 - - - name: Extract version from tag - id: version - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - VERSION="${TAG#v}" - # Strip pre-release/build metadata for MSI (only supports numeric X.Y.Z) - NUMERIC_VERSION="${VERSION%%-*}" - NUMERIC_VERSION="${NUMERIC_VERSION%%+*}" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "numeric_version=$NUMERIC_VERSION" >> "$GITHUB_OUTPUT" - echo "Resolved version: $VERSION (MSI: $NUMERIC_VERSION)" - - - name: Sync version to tauri.conf.json and Cargo.toml - shell: bash - run: | - VERSION="${{ steps.version.outputs.version }}" - TAURI_CONF="apps/desktop/src-tauri/tauri.conf.json" - CARGO_TOML="apps/desktop/src-tauri/Cargo.toml" - - node -e " - const fs = require('fs'); - const conf = JSON.parse(fs.readFileSync('$TAURI_CONF', 'utf8')); - conf.version = '$VERSION'; - fs.writeFileSync('$TAURI_CONF', JSON.stringify(conf, null, 2) + '\n'); - " - - sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" "$CARGO_TOML" - rm -f "$CARGO_TOML.bak" - - echo "Updated $TAURI_CONF version to $VERSION" - echo "Updated $CARGO_TOML version to $VERSION" - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: pnpm - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Rust cache - uses: swatinem/rust-cache@v2 - with: - workspaces: apps/desktop/src-tauri -> target - - - name: Install Linux dependencies - if: matrix.platform == 'ubuntu-22.04' - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf - - - name: Install JS dependencies - run: pnpm install --frozen-lockfile - - # macOS code signing with certificate secrets. - - name: Prepare macOS code signing - if: ${{ matrix.platform == 'macos-latest' && env.APPLE_CERTIFICATE != '' && env.APPLE_CERTIFICATE_PASSWORD != '' }} - run: | - set -euo pipefail - KEYCHAIN_PASSWORD="github-actions-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}" - echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security set-keychain-settings -t 3600 -u build.keychain - security import "$RUNNER_TEMP/certificate.p12" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - curl -fsSL -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer - security add-certificates -k build.keychain "$RUNNER_TEMP/DeveloperIDG2CA.cer" - security find-identity -v -p codesigning build.keychain - IDENTITY=$(security find-identity -v -p codesigning build.keychain | awk -F'"' '/Developer ID Application/ { print $2; exit }') - if [[ -z "$IDENTITY" ]]; then - echo "No Developer ID Application identity in build keychain" - exit 1 - fi - echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" - - - name: Prepare Tauri build args - id: tauri-args - shell: bash - run: | - ARGS="--target ${{ matrix.target }}" - if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then - MSI_VERSION="${{ steps.version.outputs.numeric_version }}" - ARGS="$ARGS --config {\"bundle\":{\"windows\":{\"wix\":{\"version\":\"$MSI_VERSION\"}}}}" - fi - echo "args=$ARGS" >> "$GITHUB_OUTPUT" - - - name: Build and release Tauri app - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - projectPath: apps/desktop - args: ${{ steps.tauri-args.outputs.args }} - tauriScript: pnpm tauri - tagName: ${{ github.ref_name }} - releaseName: "Memoh Desktop ${{ github.ref_name }}" - releaseBody: "See the [full changelog](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." - releaseDraft: true - prerelease: false