Skip to content

Call Electron Forge via Node CLI entry #464

Call Electron Forge via Node CLI entry

Call Electron Forge via Node CLI entry #464

Workflow file for this run

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