Desktop Nightly #166
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: Desktop Nightly | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 0 * * *" | |
| permissions: | |
| contents: write | |
| actions: write | |
| pull-requests: read | |
| concurrency: | |
| group: desktop-nightly | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| uses: ./.github/workflows/desktop-build.yml | |
| with: | |
| environment: nexu-test | |
| sentry_env: "test" | |
| cloud_url: "https://nexu.powerformer.net" | |
| link_url: "https://nexu-link.powerformer.net" | |
| update_feed_url: "https://desktop-releases.nexu.io/nightly" | |
| build_source: "nightly" | |
| release_tag: desktop-nightly | |
| release_name: "Nexu Desktop Nightly" | |
| channel: "nightly" | |
| secrets: inherit | |
| package-windows: | |
| runs-on: windows-latest | |
| env: | |
| CHANNEL: nightly | |
| UPDATE_FEED_ROOT: https://desktop-releases.nexu.io/nightly | |
| LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} | |
| LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} | |
| LANGFUSE_BASE_URL: ${{ secrets.LANGFUSE_BASE_URL }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.26.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Install packaging tools | |
| shell: pwsh | |
| run: | | |
| choco install nsis awscli -y --no-progress | |
| - name: Install dependencies | |
| shell: pwsh | |
| run: pnpm install --frozen-lockfile | |
| - name: Resolve build metadata | |
| id: meta | |
| shell: pwsh | |
| run: | | |
| $baseVersion = (node apps/desktop/scripts/desktop-package-version.mjs get).Trim() | |
| $buildDate = Get-Date -Format 'yyyyMMdd' | |
| $shortSha = "${env:GITHUB_SHA}".Substring(0, 7) | |
| $desktopVersion = "$baseVersion-$env:CHANNEL.$buildDate" | |
| node apps/desktop/scripts/desktop-package-version.mjs set "$desktopVersion" | Out-Null | |
| "desktop_version=$desktopVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "build_date=$buildDate" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "short_sha=$shortSha" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Build Windows installer | |
| shell: pwsh | |
| env: | |
| NEXU_CLOUD_URL: https://nexu.powerformer.net | |
| NEXU_LINK_URL: https://nexu-link.powerformer.net | |
| NEXU_DESKTOP_BUILD_SOURCE: nightly | |
| NEXU_DESKTOP_BUILD_BRANCH: ${{ github.ref_name }} | |
| NEXU_DESKTOP_BUILD_COMMIT: ${{ github.sha }} | |
| NEXU_UPDATE_FEED_URL: https://desktop-releases.nexu.io/nightly/win32/x64/latest-win.json | |
| VITE_POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} | |
| POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} | |
| run: | | |
| $env:NEXU_DESKTOP_BUILD_TIME = (Get-Date).ToUniversalTime().ToString('o') | |
| pnpm --filter @nexu/desktop dist:win | |
| - name: Prepare Windows release artifacts | |
| id: artifacts | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ steps.meta.outputs.desktop_version }} | |
| SHORT_SHA: ${{ steps.meta.outputs.short_sha }} | |
| CHANNEL: nightly | |
| BASE_URL: https://desktop-releases.nexu.io/nightly/win32/x64 | |
| run: | | |
| $artifactVersion = "$env:VERSION.$env:SHORT_SHA" | |
| $releaseDir = "apps/desktop/release" | |
| $channelArtifacts = "apps/desktop/channel-artifacts-win" | |
| New-Item -ItemType Directory -Force -Path $channelArtifacts | Out-Null | |
| Get-ChildItem -Path $channelArtifacts -File -ErrorAction SilentlyContinue | Remove-Item -Force | |
| $sourceInstaller = Join-Path $releaseDir "nexu-setup-$env:VERSION-x64.exe" | |
| if (-not (Test-Path $sourceInstaller)) { | |
| throw "Missing Windows installer: $sourceInstaller" | |
| } | |
| $versionedInstaller = "nexu-setup-$artifactVersion-win-x64.exe" | |
| $latestInstaller = "nexu-latest-$env:CHANNEL-win-x64.exe" | |
| $checksumFile = "desktop-win-x64-sha256.txt" | |
| $manifestFile = "latest-win.json" | |
| Copy-Item $sourceInstaller (Join-Path $channelArtifacts $versionedInstaller) | |
| Copy-Item $sourceInstaller (Join-Path $channelArtifacts $latestInstaller) | |
| $hash = (Get-FileHash -Algorithm SHA256 (Join-Path $channelArtifacts $versionedInstaller)).Hash.ToLowerInvariant() | |
| "$hash $versionedInstaller" | Out-File -FilePath (Join-Path $channelArtifacts $checksumFile) -Encoding ascii | |
| $env:INSTALLER_FILE = (Join-Path $channelArtifacts $latestInstaller) | |
| $env:MANIFEST_OUTPUT = (Join-Path $channelArtifacts $manifestFile) | |
| node apps/desktop/scripts/generate-win-update-manifest.mjs | |
| "versioned_installer=$versionedInstaller" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "latest_installer=$latestInstaller" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "checksum_file=$checksumFile" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "manifest_file=$manifestFile" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Upload Windows workflow artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-nightly-win-x64-${{ steps.meta.outputs.build_date }}-${{ steps.meta.outputs.short_sha }} | |
| path: | | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.versioned_installer }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.latest_installer }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.checksum_file }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.manifest_file }} | |
| retention-days: 7 | |
| if-no-files-found: error | |
| - name: Publish Windows prerelease assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: desktop-nightly | |
| target_commitish: ${{ github.sha }} | |
| name: Nexu Desktop Nightly | |
| prerelease: true | |
| draft: false | |
| overwrite_files: true | |
| fail_on_unmatched_files: true | |
| files: | | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.versioned_installer }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.latest_installer }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.checksum_file }} | |
| apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.manifest_file }} | |
| - name: Upload Windows artifacts to Cloudflare R2 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| AWS_ENDPOINT_URL: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | |
| AWS_REGION: auto | |
| shell: pwsh | |
| run: | | |
| $prefix = "nightly/win32/x64" | |
| aws s3 cp "apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.versioned_installer }}" "s3://nexu-desktop-releases/$prefix/${{ steps.artifacts.outputs.versioned_installer }}" --no-progress | |
| aws s3 cp "apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.latest_installer }}" "s3://nexu-desktop-releases/$prefix/${{ steps.artifacts.outputs.latest_installer }}" --no-progress | |
| aws s3 cp "apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.checksum_file }}" "s3://nexu-desktop-releases/$prefix/${{ steps.artifacts.outputs.checksum_file }}" --no-progress | |
| aws s3 cp "apps/desktop/channel-artifacts-win/${{ steps.artifacts.outputs.manifest_file }}" "s3://nexu-desktop-releases/$prefix/${{ steps.artifacts.outputs.manifest_file }}" --no-progress | |
| - name: Purge Windows latest CDN artifacts | |
| env: | |
| CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| CLOUDFLARE_PURGE_API_TOKEN: ${{ secrets.CLOUDFLARE_PURGE_API_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| if ([string]::IsNullOrWhiteSpace($env:CLOUDFLARE_ZONE_ID) -or [string]::IsNullOrWhiteSpace($env:CLOUDFLARE_PURGE_API_TOKEN)) { | |
| Write-Host "Skipping Cloudflare purge because required secrets are missing" | |
| exit 0 | |
| } | |
| $baseUrl = "https://desktop-releases.nexu.io/nightly/win32/x64" | |
| $files = @( | |
| "$baseUrl/${{ steps.artifacts.outputs.latest_installer }}", | |
| "$baseUrl/${{ steps.artifacts.outputs.manifest_file }}" | |
| ) | ConvertTo-Json | |
| $payload = @{ files = ($files | ConvertFrom-Json) } | ConvertTo-Json -Depth 5 | |
| Invoke-RestMethod -Method Post -Uri "https://api.cloudflare.com/client/v4/zones/$env:CLOUDFLARE_ZONE_ID/purge_cache" -Headers @{ Authorization = "Bearer $env:CLOUDFLARE_PURGE_API_TOKEN" } -ContentType "application/json" -Body $payload | Out-Null | |
| - name: Publish Windows download links | |
| shell: pwsh | |
| run: | | |
| $baseUrl = "https://desktop-releases.nexu.io/nightly/win32/x64" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "## Windows Nightly Downloads" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- Installer: $baseUrl/${{ steps.artifacts.outputs.latest_installer }}" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- Manifest: $baseUrl/${{ steps.artifacts.outputs.manifest_file }}" | |
| trigger-e2e: | |
| needs: [build, package-windows] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Trigger Desktop E2E (async) | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'desktop-e2e.yml', | |
| ref: context.ref || 'main', | |
| inputs: { mode: 'model', channel: 'nightly' }, | |
| }); | |
| notify: | |
| needs: [build, package-windows] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Resolve build metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| base_version=$(node -e 'process.stdout.write(require("./apps/desktop/package.json").version)') | |
| build_date=$(date +"%Y%m%d") | |
| short_sha="${GITHUB_SHA::7}" | |
| version="${base_version}-nightly.${build_date}" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" | |
| echo "build_date=$build_date" >> "$GITHUB_OUTPUT" | |
| - name: Generate changelog since last release | |
| id: changelog | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| repo_url="https://github.com/${REPO}" | |
| # Find the latest stable release tag (vX.Y.Z, not desktop-nightly) | |
| last_release=$(git tag -l 'v*' --sort=-creatordate | head -1) | |
| if [ -z "$last_release" ]; then | |
| echo "No release tag found, using first commit" | |
| last_release=$(git rev-list --max-parents=0 HEAD) | |
| fi | |
| echo "Comparing: ${last_release}..HEAD" | |
| echo "last_release=$last_release" >> "$GITHUB_OUTPUT" | |
| # Extract merged PR numbers from both formats: | |
| # Squash merge: "feat: add foo (#123)" → matches (#123) | |
| # Merge commit: "Merge pull request #123 from ..." → matches #123 | |
| pr_numbers=$(git log "${last_release}..HEAD" --oneline \ | |
| | grep -oE '(\(#[0-9]+\)|Merge pull request #[0-9]+)' \ | |
| | grep -oE '[0-9]+' | sort -un || true) | |
| # Output files: feishu (with feishu markdown links), github (GFM), plain (for LLM) | |
| feishu_lines="" | |
| github_lines="" | |
| plain_lines="" | |
| pr_count=0 | |
| for pr in $pr_numbers; do | |
| pr_data=$(gh pr view "$pr" --json title,labels,closingIssuesReferences \ | |
| --jq '{ | |
| title: .title, | |
| labels: [.labels[].name] | join(","), | |
| issues: [.closingIssuesReferences[].number] | |
| }' 2>/dev/null || echo "") | |
| [ -z "$pr_data" ] && continue | |
| title=$(echo "$pr_data" | jq -r '.title') | |
| labels=$(echo "$pr_data" | jq -r '.labels') | |
| # Extract area from conventional commit prefix or labels | |
| area="" | |
| if echo "$title" | grep -qE '^\w+\(([^)]+)\)'; then | |
| area=$(echo "$title" | sed -E 's/^\w+\(([^)]+)\).*/\1/') | |
| fi | |
| display_area="$area" | |
| for kw_label in desktop web controller skills channels models ci; do | |
| if echo "$labels" | grep -qi "$kw_label"; then | |
| display_area="$kw_label" | |
| break | |
| fi | |
| done | |
| tag=""; [ -n "$display_area" ] && tag="\`${display_area}\` " | |
| # Clean title: remove redundant "scope(area): " prefix for display | |
| clean_title=$(echo "$title" | sed -E 's/^\w+(\([^)]+\))?: ?//') | |
| # Resolve closing issue titles (fetch individually to avoid null) | |
| issue_nums=$(echo "$pr_data" | jq -r '.issues[]' 2>/dev/null || true) | |
| issue_parts_feishu="" | |
| issue_parts_github="" | |
| issue_parts_plain="" | |
| for inum in $issue_nums; do | |
| ititle=$(gh issue view "$inum" --json title --jq '.title' 2>/dev/null || echo "") | |
| [ -z "$ititle" ] && continue | |
| issue_parts_feishu="${issue_parts_feishu} 📌 [#${inum}](${repo_url}/issues/${inum}) ${ititle}\n" | |
| issue_parts_github="${issue_parts_github} - 📌 #${inum} ${ititle}\n" | |
| issue_parts_plain="${issue_parts_plain} 关联: #${inum} ${ititle}\n" | |
| done | |
| # Build lines | |
| feishu_lines="${feishu_lines}${tag}${clean_title} ([PR #${pr}](${repo_url}/pull/${pr}))\n${issue_parts_feishu}" | |
| github_lines="${github_lines}- ${tag}${clean_title} (#${pr})\n${issue_parts_github}" | |
| plain_lines="${plain_lines}${display_area:+[${display_area}] }${clean_title} #${pr}\n${issue_parts_plain}" | |
| pr_count=$((pr_count + 1)) | |
| done | |
| if [ "$pr_count" -eq 0 ]; then | |
| feishu_lines="No merged PRs since ${last_release}" | |
| github_lines="No merged PRs since ${last_release}" | |
| plain_lines="No merged PRs since ${last_release}" | |
| fi | |
| echo "pr_count=$pr_count" >> "$GITHUB_OUTPUT" | |
| printf '%b' "$feishu_lines" > /tmp/changelog_feishu.txt | |
| printf '%b' "$github_lines" > /tmp/changelog_github.txt | |
| printf '%b' "$plain_lines" > /tmp/changelog_plain.txt | |
| echo "Generated changelog with $pr_count PRs" | |
| - name: Write GitHub Step Summary | |
| env: | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| SHORT_SHA: ${{ steps.meta.outputs.short_sha }} | |
| PR_COUNT: ${{ steps.changelog.outputs.pr_count }} | |
| LAST_RELEASE: ${{ steps.changelog.outputs.last_release }} | |
| shell: bash | |
| run: | | |
| { | |
| echo "## Nightly Changelog" | |
| echo "" | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| **Version** | \`${VERSION}\` (\`${SHORT_SHA}\`) |" | |
| echo "| **Baseline** | \`${LAST_RELEASE}\` |" | |
| echo "| **PRs merged** | ${PR_COUNT} |" | |
| echo "" | |
| echo "### Changes since ${LAST_RELEASE}" | |
| echo "" | |
| cat /tmp/changelog_github.txt | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Generate LLM summary | |
| id: llm | |
| env: | |
| OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| changelog=$(cat /tmp/changelog_plain.txt) | |
| pr_count="${{ steps.changelog.outputs.pr_count }}" | |
| if [ "$pr_count" -eq 0 ] || [ -z "$OPENAI_BASE_URL" ] || [ -z "$OPENAI_API_KEY" ]; then | |
| echo "summary=无新增变更" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Ask LLM for a concise Chinese summary (same endpoint as nexu-pal) | |
| prompt="你是一个桌面应用的发布助手。以下是自上次发布以来合并的 PR 列表(带关联 issue):\n\n${changelog}\n\n请用中文生成一段简洁的变更摘要(3-5 句话),重点说明用户可感知的改进和修复。不要列举 PR 编号,用自然语言概括。如果有 bug 修复请优先提及。" | |
| response=$(curl -sf --max-time 30 "${OPENAI_BASE_URL}/chat/completions" \ | |
| -H "Authorization: Bearer ${OPENAI_API_KEY}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n --arg prompt "$prompt" '{ | |
| model: "google/gemini-2.5-flash", | |
| messages: [{role: "user", content: $prompt}], | |
| max_tokens: 300, | |
| temperature: 0.3 | |
| }')" 2>/dev/null || echo "") | |
| if [ -n "$response" ]; then | |
| summary=$(echo "$response" | jq -r '.choices[0].message.content // empty' 2>/dev/null || echo "") | |
| fi | |
| if [ -z "${summary:-}" ]; then | |
| summary="本次 nightly 包含 ${pr_count} 个 PR 变更,详见下方列表。" | |
| fi | |
| # Escape for GitHub output | |
| { | |
| echo "summary<<EOFLLM" | |
| echo "$summary" | |
| echo "EOFLLM" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Send Feishu notification | |
| env: | |
| FEISHU_WEBHOOK: ${{ secrets.NIGHTLY_FEISHU_WEBHOOK }} | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| SHORT_SHA: ${{ steps.meta.outputs.short_sha }} | |
| BUILD_DATE: ${{ steps.meta.outputs.build_date }} | |
| PR_COUNT: ${{ steps.changelog.outputs.pr_count }} | |
| LAST_RELEASE: ${{ steps.changelog.outputs.last_release }} | |
| LLM_SUMMARY: ${{ steps.llm.outputs.summary }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$FEISHU_WEBHOOK" ]; then | |
| echo "No Feishu webhook configured, skipping notification" | |
| exit 0 | |
| fi | |
| changelog=$(cat /tmp/changelog_feishu.txt | head -40) | |
| run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| release_url="https://github.com/${{ github.repository }}/releases/tag/desktop-nightly" | |
| e2e_url="https://github.com/${{ github.repository }}/actions/workflows/desktop-e2e.yml" | |
| dmg_arm64="https://desktop-releases.nexu.io/nightly/arm64/nexu-latest-nightly-mac-arm64.dmg" | |
| dmg_x64="https://desktop-releases.nexu.io/nightly/x64/nexu-latest-nightly-mac-x64.dmg" | |
| # Build Feishu interactive card | |
| card=$(jq -n \ | |
| --arg version "$VERSION" \ | |
| --arg sha "$SHORT_SHA" \ | |
| --arg date "$BUILD_DATE" \ | |
| --arg pr_count "$PR_COUNT" \ | |
| --arg last_release "$LAST_RELEASE" \ | |
| --arg summary "$LLM_SUMMARY" \ | |
| --arg changelog "$changelog" \ | |
| --arg run_url "$run_url" \ | |
| --arg release_url "$release_url" \ | |
| --arg e2e_url "$e2e_url" \ | |
| --arg dmg_arm64 "$dmg_arm64" \ | |
| --arg dmg_x64 "$dmg_x64" \ | |
| '{ | |
| msg_type: "interactive", | |
| card: { | |
| header: { | |
| template: "turquoise", | |
| title: { | |
| tag: "plain_text", | |
| content: ("🚀 Nexu Desktop Nightly " + $version) | |
| } | |
| }, | |
| elements: [ | |
| { | |
| tag: "markdown", | |
| content: ("**变更摘要**\n" + $summary) | |
| }, | |
| { | |
| tag: "markdown", | |
| content: ("**版本信息**\n- 版本: `" + $version + "` (`" + $sha + "`)\n- 基线: `" + $last_release + "`\n- 合并 PR: **" + $pr_count + "** 个") | |
| }, | |
| { | |
| tag: "markdown", | |
| content: ("**变更列表**\n" + $changelog) | |
| }, | |
| { | |
| tag: "hr" | |
| }, | |
| { | |
| tag: "action", | |
| actions: [ | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "⬇️ DMG (Apple Silicon)" }, | |
| type: "primary", | |
| url: $dmg_arm64 | |
| }, | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "⬇️ DMG (Intel)" }, | |
| type: "default", | |
| url: $dmg_x64 | |
| } | |
| ] | |
| }, | |
| { | |
| tag: "action", | |
| actions: [ | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "📦 CI 构建" }, | |
| type: "default", | |
| url: $run_url | |
| }, | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "🧪 E2E 测试" }, | |
| type: "default", | |
| url: $e2e_url | |
| }, | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "📋 Release" }, | |
| type: "default", | |
| url: $release_url | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| }') | |
| curl -sf -X POST "$FEISHU_WEBHOOK" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$card" | |
| echo "Feishu notification sent" |