Log Forge package output discovery diagnostics #467
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: Build Hagicode Desktop | |
| on: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| inputs: | |
| production_build: | |
| description: Run a production-signed build and upload results as workflow artifacts | |
| required: false | |
| default: false | |
| type: boolean | |
| channel: | |
| description: Release channel (stable, beta, dev) | |
| required: false | |
| type: choice | |
| options: | |
| - stable | |
| - beta | |
| - dev | |
| sign_windows_release: | |
| description: Sign Windows release artifacts in this manual run | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| id-token: write | |
| jobs: | |
| prepare-release: | |
| name: Prepare Release Metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.meta.outputs.version }} | |
| release_tag: ${{ steps.meta.outputs.release_tag }} | |
| release_name: ${{ steps.meta.outputs.release_name }} | |
| channel: ${{ steps.meta.outputs.channel }} | |
| channel_source: ${{ steps.meta.outputs.channel_source }} | |
| version_source: ${{ steps.meta.outputs.version_source }} | |
| is_tag_release: ${{ steps.meta.outputs.is_tag_release }} | |
| is_production_build: ${{ steps.meta.outputs.is_production_build }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve main branch version from Release Drafter draft | |
| id: draft | |
| if: github.ref == 'refs/heads/main' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| GH_SHA: ${{ github.sha }} | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY2' | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import time | |
| import urllib.request | |
| semver_re = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") | |
| def parse(tag: str): | |
| match = semver_re.fullmatch(tag or "") | |
| if not match: | |
| return None | |
| return tuple(int(part) for part in match.groups()) | |
| def gh_api(path: str): | |
| request = urllib.request.Request( | |
| f"https://api.github.com/{path}", | |
| headers={ | |
| "Authorization": f"Bearer {os.environ['GH_TOKEN']}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "User-Agent": "hagicode-desktop-build", | |
| }, | |
| ) | |
| with urllib.request.urlopen(request) as response: | |
| return json.load(response) | |
| repo = os.environ["GH_REPO"] | |
| sha = os.environ["GH_SHA"] | |
| event_name = os.environ["GITHUB_EVENT_NAME"] | |
| if event_name == "push": | |
| deadline = time.time() + 300 | |
| while time.time() < deadline: | |
| runs = gh_api(f"repos/{repo}/actions/workflows/release-drafter.yml/runs?head_sha={sha}&event=push&per_page=10") | |
| candidates = [ | |
| run for run in runs.get("workflow_runs", []) | |
| if run.get("path") == ".github/workflows/release-drafter.yml" and run.get("head_sha") == sha | |
| ] | |
| if candidates: | |
| release_drafter_run = candidates[0] | |
| if release_drafter_run.get("status") == "completed": | |
| conclusion = release_drafter_run.get("conclusion") | |
| if conclusion != "success": | |
| raise SystemExit(f"Release Drafter workflow for {sha} concluded with {conclusion}") | |
| break | |
| time.sleep(5) | |
| else: | |
| raise SystemExit(f"Timed out waiting for Release Drafter workflow for {sha}") | |
| releases = gh_api(f"repos/{repo}/releases?per_page=100") | |
| draft_candidates = [ | |
| release | |
| for release in releases | |
| if release.get("draft") and parse(release.get("tag_name", "")) | |
| ] | |
| version = None | |
| version_source = None | |
| if draft_candidates: | |
| selected = max(draft_candidates, key=lambda item: parse(item["tag_name"])) | |
| release_tag = selected["tag_name"] | |
| version = release_tag.removeprefix("v") | |
| version_source = f"draft-release:{release_tag}" | |
| else: | |
| tags = subprocess.check_output( | |
| ["git", "tag", "--list", "v*", "--sort=-version:refname"], | |
| text=True, | |
| ).splitlines() | |
| stable_tags = [tag for tag in tags if parse(tag)] | |
| if stable_tags: | |
| latest_tag = stable_tags[0] | |
| major, minor, patch = parse(latest_tag) | |
| version = f"{major}.{minor}.{patch + 1}" | |
| version_source = f"latest-tag:{latest_tag}" | |
| else: | |
| package_json = json.loads(open("package.json", "r", encoding="utf-8").read()) | |
| package_version = package_json["version"] | |
| package_tag = f"v{package_version}" | |
| parsed_package = parse(package_tag) | |
| if not parsed_package: | |
| raise SystemExit(f"package.json version is not a stable semver: {package_version}") | |
| major, minor, patch = parsed_package | |
| version = f"{major}.{minor}.{patch + 1}" | |
| version_source = f"package-json:{package_version}" | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle: | |
| handle.write(f"resolved_version={version}\n") | |
| handle.write(f"version_source={version_source}\n") | |
| PY2 | |
| - name: Resolve release metadata | |
| id: meta | |
| shell: bash | |
| env: | |
| MANUAL_CHANNEL: ${{ github.event.inputs.channel || '' }} | |
| MANUAL_PRODUCTION_BUILD: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.production_build == 'true' && 'true' || 'false' }} | |
| DRAFT_VERSION: ${{ steps.draft.outputs.resolved_version || '' }} | |
| DRAFT_VERSION_SOURCE: ${{ steps.draft.outputs.version_source || '' }} | |
| run: | | |
| set -euo pipefail | |
| package_version="$(node -p "require('./package.json').version")" | |
| draft_version="${DRAFT_VERSION}" | |
| version="${package_version}" | |
| release_tag='' | |
| release_name="Build ${package_version}" | |
| version_source="package-json:${package_version}" | |
| is_tag_release='false' | |
| is_production_build='false' | |
| if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then | |
| version="${GITHUB_REF_NAME#v}" | |
| release_tag="${GITHUB_REF_NAME}" | |
| release_name="Release ${release_tag}" | |
| version_source="tag:${GITHUB_REF_NAME}" | |
| is_tag_release='true' | |
| is_production_build='true' | |
| elif [[ "${GITHUB_REF}" == 'refs/heads/main' ]]; then | |
| if [[ -z "${draft_version}" ]]; then | |
| echo '::error::Release Drafter did not return resolved_version for the main branch build.' | |
| exit 1 | |
| fi | |
| version="${draft_version}" | |
| release_name="Main Build ${version}" | |
| version_source="${DRAFT_VERSION_SOURCE:-draft-release:${version}}" | |
| fi | |
| if [[ "${MANUAL_PRODUCTION_BUILD}" == 'true' ]]; then | |
| is_production_build='true' | |
| if [[ "${is_tag_release}" != 'true' ]]; then | |
| release_name="Production Build ${version}" | |
| fi | |
| fi | |
| if [[ -n "${MANUAL_CHANNEL}" ]]; then | |
| channel="${MANUAL_CHANNEL}" | |
| channel_source='manual-input' | |
| elif [[ "${MANUAL_PRODUCTION_BUILD}" == 'true' ]]; then | |
| channel='stable' | |
| channel_source='manual-production-build' | |
| elif [[ "${is_tag_release}" == 'true' ]]; then | |
| if [[ "${version}" =~ -(beta|rc) ]]; then | |
| channel='beta' | |
| elif [[ "${version}" =~ -(alpha|dev) ]]; then | |
| channel='dev' | |
| else | |
| channel='stable' | |
| fi | |
| channel_source="tag:${GITHUB_REF_NAME}" | |
| elif [[ "${GITHUB_REF}" == 'refs/heads/main' ]]; then | |
| channel='beta' | |
| channel_source='branch:main' | |
| else | |
| channel='dev' | |
| channel_source="ref:${GITHUB_REF}" | |
| fi | |
| { | |
| echo "version=${version}" | |
| echo "release_tag=${release_tag}" | |
| echo "release_name=${release_name}" | |
| echo "channel=${channel}" | |
| echo "channel_source=${channel_source}" | |
| echo "version_source=${version_source}" | |
| echo "is_tag_release=${is_tag_release}" | |
| echo "is_production_build=${is_production_build}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Write workflow summary | |
| shell: bash | |
| run: | | |
| { | |
| echo '## Release metadata prepared' | |
| echo | |
| echo '- Version: ${{ steps.meta.outputs.version }}' | |
| echo '- Release tag: ${{ steps.meta.outputs.release_tag || '(none)' }}' | |
| echo '- Release name: ${{ steps.meta.outputs.release_name }}' | |
| echo '- Release channel: ${{ steps.meta.outputs.channel }}' | |
| echo '- Channel source: ${{ steps.meta.outputs.channel_source }}' | |
| echo '- Version source: ${{ steps.meta.outputs.version_source }}' | |
| echo '- Tag release: ${{ steps.meta.outputs.is_tag_release }}' | |
| echo '- Production build: ${{ steps.meta.outputs.is_production_build }}' | |
| echo '- Ref: ${{ github.ref }}' | |
| echo '- Commit: ${{ github.sha }}' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| build-windows: | |
| name: Build Windows Packages | |
| needs: prepare-release | |
| uses: ./.github/workflows/reusable-build-windows.yml | |
| with: | |
| version: ${{ needs.prepare-release.outputs.version }} | |
| channel: ${{ needs.prepare-release.outputs.channel }} | |
| is_production_build: ${{ needs.prepare-release.outputs.is_production_build == 'true' }} | |
| sign_windows_release: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sign_windows_release == 'true' }} | |
| publish_release_assets: ${{ needs.prepare-release.outputs.is_tag_release == 'true' }} | |
| secrets: inherit | |
| build-unix: | |
| name: Build Linux and macOS Packages | |
| needs: prepare-release | |
| uses: ./.github/workflows/reusable-build-unix.yml | |
| with: | |
| version: ${{ needs.prepare-release.outputs.version }} | |
| channel: ${{ needs.prepare-release.outputs.channel }} | |
| is_production_build: ${{ needs.prepare-release.outputs.is_production_build == 'true' }} | |
| publish_release_assets: ${{ needs.prepare-release.outputs.is_tag_release == 'true' }} | |
| secrets: inherit | |
| publish-windows-release: | |
| name: Publish Windows Release Assets | |
| needs: | |
| - prepare-release | |
| - build-windows | |
| if: ${{ needs.prepare-release.outputs.is_tag_release == 'true' && needs.build-windows.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: production | |
| deployment: false | |
| steps: | |
| - name: Download Windows release bundle | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: release-windows-*-assets | |
| merge-multiple: true | |
| path: release-assets/windows | |
| - name: Upload Windows assets to GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare-release.outputs.release_tag }} | |
| overwrite_files: 'true' | |
| files: | | |
| release-assets/windows/**/*.exe | |
| release-assets/windows/**/*.zip | |
| release-assets/windows/**/*.msix | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-platform-release: | |
| name: Publish ${{ matrix.target.name }} Release Assets | |
| needs: | |
| - prepare-release | |
| - build-unix | |
| if: ${{ needs.prepare-release.outputs.is_tag_release == 'true' && needs.build-unix.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: production | |
| deployment: false | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - id: linux-appimage | |
| name: Linux AppImage | |
| artifact_path: release-assets/linux-appimage | |
| upload_glob: '**/*.AppImage' | |
| - id: linux-tar-gz | |
| name: Linux tar.gz | |
| artifact_path: release-assets/linux-tar-gz | |
| upload_glob: '**/*.tar.gz' | |
| - id: linux-zip | |
| name: Linux ZIP | |
| artifact_path: release-assets/linux-zip | |
| upload_glob: '**/*.zip' | |
| - id: macos-x64-dmg | |
| name: macOS x64 DMG | |
| artifact_path: release-assets/macos-x64-dmg | |
| upload_glob: '**/*.dmg' | |
| - id: macos-x64-zip | |
| name: macOS x64 ZIP | |
| artifact_path: release-assets/macos-x64-zip | |
| upload_glob: '**/*.zip' | |
| - id: macos-arm64-dmg | |
| name: macOS arm64 DMG | |
| artifact_path: release-assets/macos-arm64-dmg | |
| upload_glob: '**/*.dmg' | |
| - id: macos-arm64-zip | |
| name: macOS arm64 ZIP | |
| artifact_path: release-assets/macos-arm64-zip | |
| upload_glob: '**/*.zip' | |
| steps: | |
| - name: Download Linux release bundle | |
| if: startsWith(matrix.target.id, 'linux-') | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-${{ matrix.target.id }}-assets | |
| path: ${{ matrix.target.artifact_path }} | |
| - name: Download macOS release bundle | |
| if: startsWith(matrix.target.id, 'macos-') | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-${{ matrix.target.id }}-assets | |
| path: ${{ matrix.target.artifact_path }} | |
| - name: Upload Linux assets to GitHub Release | |
| if: startsWith(matrix.target.id, 'linux-') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare-release.outputs.release_tag }} | |
| overwrite_files: 'true' | |
| files: | | |
| ${{ matrix.target.artifact_path }}/${{ matrix.target.upload_glob }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload macOS assets to GitHub Release | |
| if: startsWith(matrix.target.id, 'macos-') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare-release.outputs.release_tag }} | |
| overwrite_files: 'true' | |
| files: | | |
| ${{ matrix.target.artifact_path }}/${{ matrix.target.upload_glob }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| build-summary: | |
| name: Build Summary | |
| outputs: | |
| channel: ${{ needs.prepare-release.outputs.channel }} | |
| release_status: ${{ steps.status.outputs.overall }} | |
| sync_eligible: ${{ steps.status.outputs.sync_eligible }} | |
| needs: | |
| - prepare-release | |
| - build-windows | |
| - build-unix | |
| - publish-windows-release | |
| - publish-platform-release | |
| if: always() | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: ${{ needs.prepare-release.outputs.is_production_build == 'true' && 'production' || 'ci' }} | |
| deployment: false | |
| steps: | |
| - name: Determine workflow status | |
| id: status | |
| if: always() | |
| shell: bash | |
| env: | |
| IS_TAG_RELEASE: ${{ needs.prepare-release.outputs.is_tag_release }} | |
| BUILD_WINDOWS_STATUS: ${{ needs.build-windows.result }} | |
| BUILD_UNIX_STATUS: ${{ needs.build-unix.result }} | |
| PUBLISH_WINDOWS_STATUS: ${{ needs.publish-windows-release.result }} | |
| PUBLISH_PLATFORMS_STATUS: ${{ needs.publish-platform-release.result }} | |
| run: | | |
| set -euo pipefail | |
| windows_build_status="${BUILD_WINDOWS_STATUS}" | |
| unix_build_status="${BUILD_UNIX_STATUS}" | |
| windows_publish_status="${PUBLISH_WINDOWS_STATUS}" | |
| platform_publish_status="${PUBLISH_PLATFORMS_STATUS}" | |
| echo "Windows build: ${windows_build_status}" | |
| echo "Unix build: ${unix_build_status}" | |
| if [ "${IS_TAG_RELEASE}" = 'true' ]; then | |
| echo "Windows publish: ${windows_publish_status}" | |
| echo "Unix publish: ${platform_publish_status}" | |
| fi | |
| echo "windows_build_status=${windows_build_status}" >> "$GITHUB_OUTPUT" | |
| echo "unix_build_status=${unix_build_status}" >> "$GITHUB_OUTPUT" | |
| echo "windows_publish_status=${windows_publish_status}" >> "$GITHUB_OUTPUT" | |
| echo "platform_publish_status=${platform_publish_status}" >> "$GITHUB_OUTPUT" | |
| if [ "${windows_build_status}" != 'success' ] || [ "${unix_build_status}" != 'success' ]; then | |
| echo 'overall=failed' >> "$GITHUB_OUTPUT" | |
| echo 'status_icon=❌' >> "$GITHUB_OUTPUT" | |
| echo 'status_text=失败' >> "$GITHUB_OUTPUT" | |
| echo 'sync_eligible=false' >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "${IS_TAG_RELEASE}" = 'true' ] && { [ "${windows_publish_status}" != 'success' ] || [ "${platform_publish_status}" != 'success' ]; }; then | |
| echo 'overall=failed' >> "$GITHUB_OUTPUT" | |
| echo 'status_icon=❌' >> "$GITHUB_OUTPUT" | |
| echo 'status_text=失败' >> "$GITHUB_OUTPUT" | |
| echo 'sync_eligible=false' >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo 'overall=success' >> "$GITHUB_OUTPUT" | |
| echo 'status_icon=✅' >> "$GITHUB_OUTPUT" | |
| echo 'status_text=成功' >> "$GITHUB_OUTPUT" | |
| if [ "${IS_TAG_RELEASE}" = 'true' ]; then | |
| echo 'sync_eligible=true' >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'sync_eligible=false' >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build platforms summary | |
| id: platforms | |
| if: always() | |
| shell: bash | |
| env: | |
| WINDOWS_STATUS: ${{ steps.status.outputs.windows_build_status }} | |
| UNIX_STATUS: ${{ steps.status.outputs.unix_build_status }} | |
| run: | | |
| platforms=() | |
| if [ "${WINDOWS_STATUS}" = 'success' ]; then | |
| platforms+=("Windows Portable") | |
| platforms+=("Windows NSIS") | |
| platforms+=("Windows MSIX") | |
| fi | |
| if [ "${UNIX_STATUS}" = 'success' ]; then | |
| platforms+=("Linux AppImage") | |
| platforms+=("Linux tar.gz") | |
| platforms+=("Linux ZIP") | |
| platforms+=("macOS x64") | |
| platforms+=("macOS arm64") | |
| fi | |
| if [ ${#platforms[@]} -eq 0 ]; then | |
| platforms_str='无' | |
| else | |
| platforms_str=$(IFS=', '; echo "${platforms[*]}") | |
| fi | |
| echo "platforms=${platforms_str}" >> "$GITHUB_OUTPUT" | |
| - name: Publish workflow summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| { | |
| echo '## Release orchestration summary' | |
| echo | |
| echo '- Version: ${{ needs.prepare-release.outputs.version }}' | |
| echo '- Release tag: ${{ needs.prepare-release.outputs.release_tag || '(none)' }}' | |
| echo '- Release name: ${{ needs.prepare-release.outputs.release_name }}' | |
| echo '- Version source: ${{ needs.prepare-release.outputs.version_source }}' | |
| echo '- Release channel: ${{ needs.prepare-release.outputs.channel }}' | |
| echo '- Channel source: ${{ needs.prepare-release.outputs.channel_source }}' | |
| echo '- Production build: ${{ needs.prepare-release.outputs.is_production_build }}' | |
| echo '- Windows build status: ${{ steps.status.outputs.windows_build_status }}' | |
| echo '- Unix build status: ${{ steps.status.outputs.unix_build_status }}' | |
| echo '- Windows publish status: ${{ steps.status.outputs.windows_publish_status }}' | |
| echo '- Unix publish status: ${{ steps.status.outputs.platform_publish_status }}' | |
| echo '- Normalized overall status: ${{ steps.status.outputs.overall }}' | |
| echo '- Azure sync eligible: ${{ steps.status.outputs.sync_eligible }}' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Notify Feishu | |
| if: always() | |
| uses: HagiCode-org/haginotifier@v1.0.0 | |
| with: | |
| message: | | |
| **Flow 执行完成** | |
| 状态: ${{ steps.status.outputs.status_icon }} ${{ steps.status.outputs.status_text }} | |
| 分支: ${{ github.ref_name }} | |
| 提交: `${{ github.sha }}` | |
| 触发者: ${{ github.actor }} | |
| 事件: ${{ github.event_name }} | |
| 构建平台: ${{ steps.platforms.outputs.platforms }} | |
| [查看详情](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| msg_type: post | |
| title: Hagicode Desktop 构建通知 ${{ steps.status.outputs.status_icon }} | |
| env: | |
| FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} | |
| - name: Fail workflow on normalized platform failure | |
| if: steps.status.outputs.overall != 'success' | |
| run: | | |
| echo '::error::One or more release build or upload jobs failed. Downstream publish is blocked.' | |
| exit 1 | |
| sync-azure-upload: | |
| name: Upload Release Shards to Azure Storage | |
| needs: | |
| - prepare-release | |
| - build-summary | |
| if: ${{ always() && needs.build-summary.outputs.sync_eligible == 'true' && needs.build-summary.outputs.release_status == 'success' }} | |
| uses: ./.github/workflows/sync-azure-storage.yml | |
| with: | |
| release_tag: ${{ needs.prepare-release.outputs.release_tag }} | |
| release_channel: ${{ needs.build-summary.outputs.channel }} | |
| secrets: inherit | |
| sync-azure-finalize: | |
| name: Finalize Azure Storage Sync | |
| needs: sync-azure-upload | |
| if: ${{ always() && needs.sync-azure-upload.result == 'success' }} | |
| uses: ./.github/workflows/finalize-azure-storage.yml | |
| secrets: inherit |