.exe build #146
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
| # Build Windows installer on CI (no .exe committed to git), publish GitHub Release, notify /api/releases. | |
| name: .exe build | |
| on: | |
| workflow_dispatch: | |
| # No manual inputs: release tag/build folder id is computed from current UTC time. | |
| # Format: build_MMDDYYYY_HHMM (e.g. build_03252026_1929) | |
| permissions: | |
| contents: write | |
| jobs: | |
| publish: | |
| runs-on: windows-latest | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Setup Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Compute release_id from UTC time | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| rid="build_$(date -u +%m%d%Y_%H%M)" | |
| echo "release_id=$rid" >> "$GITHUB_OUTPUT" | |
| echo "RELEASE_BUILD_ID=$rid" >> "$GITHUB_ENV" | |
| - name: Build Windows installer | |
| run: npm run build:win | |
| # cleanup.cjs writes releases/builder/<build_id>/ (installer at root; zip + yml in dev/). | |
| # Stage flat copies under releases/electron/<build_id>/ for GitHub upload. | |
| - name: Locate electron-builder artifacts and stage under releases/electron | |
| id: artifact | |
| run: | | |
| set -euo pipefail | |
| rid="${{ steps.meta.outputs.release_id }}" | |
| src_dir="releases/builder/$rid" | |
| dev_dir="$src_dir/dev" | |
| stage_dir="releases/electron/$rid" | |
| mkdir -p "$stage_dir" | |
| exe="$(ls -1 "$src_dir"/HyperlinksSpaceProgramInstaller_*.exe 2>/dev/null | head -n 1 || true)" | |
| if [[ -z "$exe" || ! -f "$exe" ]]; then | |
| echo "Expected timestamped installer in $src_dir (set RELEASE_BUILD_ID=$rid)." | |
| find releases -maxdepth 6 -type f 2>/dev/null || true | |
| exit 1 | |
| fi | |
| zip="$(ls -1 "$dev_dir"/HyperlinksSpaceProgram_*.zip 2>/dev/null | head -n 1 || true)" | |
| if [[ -z "$zip" || ! -f "$zip" ]]; then | |
| echo "Portable zip HyperlinksSpaceProgram_<version>.zip is required in $dev_dir (electron-builder zip target + cleanup)." | |
| find "$dev_dir" -maxdepth 1 -type f 2>/dev/null || true | |
| exit 1 | |
| fi | |
| yml="$dev_dir/latest.yml" | |
| zip_yml="$dev_dir/zip-latest.yml" | |
| if [[ ! -f "$yml" ]]; then | |
| echo "latest.yml missing next to installer — electron-updater needs it on the GitHub Release." | |
| exit 1 | |
| fi | |
| exe_name="$(basename "$exe")" | |
| zip_name="$(basename "$zip")" | |
| exe_out="$stage_dir/$exe_name" | |
| zip_out="$stage_dir/$zip_name" | |
| yml_out="$stage_dir/latest.yml" | |
| zip_yml_out="$stage_dir/zip-latest.yml" | |
| cp "$exe" "$exe_out" | |
| cp "$zip" "$zip_out" | |
| cp "$yml" "$yml_out" | |
| if [[ -f "$zip_yml" ]]; then | |
| cp "$zip_yml" "$zip_yml_out" | |
| fi | |
| echo "exe_path=$exe_out" >> "$GITHUB_OUTPUT" | |
| echo "exe_name=$exe_name" >> "$GITHUB_OUTPUT" | |
| echo "zip_path=$zip_out" >> "$GITHUB_OUTPUT" | |
| echo "zip_name=$zip_name" >> "$GITHUB_OUTPUT" | |
| echo "yml_path=$yml_out" >> "$GITHUB_OUTPUT" | |
| echo "zip_yml_path=$zip_yml_out" >> "$GITHUB_OUTPUT" | |
| echo "release_id=$rid" >> "$GITHUB_OUTPUT" | |
| - name: Check if GitHub release already exists | |
| id: exists | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| set -euo pipefail | |
| rid="${{ steps.artifact.outputs.release_id }}" | |
| if gh release view "$rid" --repo "${{ github.repository }}" >/dev/null 2>&1; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Preflight checks and artifact summary | |
| if: steps.exists.outputs.exists != 'true' | |
| id: preflight | |
| run: | | |
| set -euo pipefail | |
| rid="${{ steps.artifact.outputs.release_id }}" | |
| exe="${{ steps.artifact.outputs.exe_path }}" | |
| zip="${{ steps.artifact.outputs.zip_path }}" | |
| yml="${{ steps.artifact.outputs.yml_path }}" | |
| zip_yml="${{ steps.artifact.outputs.zip_yml_path }}" | |
| stage_dir="releases/electron/$rid" | |
| [[ -d "$stage_dir" ]] || { echo "Stage dir missing: $stage_dir"; exit 1; } | |
| [[ -f "$exe" ]] || { echo "Installer missing: $exe"; exit 1; } | |
| [[ -f "$zip" ]] || { echo "Portable zip missing: $zip"; exit 1; } | |
| [[ -f "$yml" ]] || { echo "latest.yml missing: $yml"; exit 1; } | |
| exe_name="$(basename "$exe")" | |
| zip_name="$(basename "$zip")" | |
| yml_name="$(basename "$yml")" | |
| [[ "$exe_name" == HyperlinksSpaceProgramInstaller_*.exe ]] || { | |
| echo "Unexpected installer name: $exe_name"; exit 1; | |
| } | |
| [[ "$zip_name" == HyperlinksSpaceProgram_*.zip ]] || { | |
| echo "Unexpected zip name: $zip_name"; exit 1; | |
| } | |
| [[ "$yml_name" == "latest.yml" ]] || { | |
| echo "Unexpected yml name: $yml_name"; exit 1; | |
| } | |
| app_version="$(echo "$zip_name" | sed -E 's/^HyperlinksSpaceProgram_(.+)\.zip$/\1/')" | |
| if [[ -z "$app_version" || "$app_version" == "$zip_name" ]]; then | |
| app_version="unknown" | |
| fi | |
| echo "app_version=$app_version" >> "$GITHUB_OUTPUT" | |
| echo "Preflight OK (electron-builder):" | |
| echo " release_id: $rid" | |
| echo " app_version: $app_version" | |
| echo " stage_dir: $stage_dir" | |
| echo " installer: $exe" | |
| echo " portable_zip: $zip" | |
| echo " latest_yml: $yml" | |
| if [[ -n "$zip_yml" && -f "$zip_yml" ]]; then | |
| echo " zip_latest_yml: $zip_yml" | |
| else | |
| echo " zip_latest_yml: (missing, optional)" | |
| fi | |
| - name: Create GitHub release and upload installer + portable zip | |
| if: steps.exists.outputs.exists != 'true' | |
| id: create | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| rid="${{ steps.artifact.outputs.release_id }}" | |
| exe="${{ steps.artifact.outputs.exe_path }}" | |
| zip="${{ steps.artifact.outputs.zip_path }}" | |
| yml="${{ steps.artifact.outputs.yml_path }}" | |
| zip_yml="${{ steps.artifact.outputs.zip_yml_path }}" | |
| upload=( "$exe" "$yml" "$zip" ) | |
| if [[ -f "$zip_yml" ]]; then | |
| upload+=( "$zip_yml" ) | |
| else | |
| echo "Warning: zip-latest.yml missing (cleanup should generate it when zip exists)." | |
| fi | |
| echo "Publishing release $rid (app_version=${{ steps.preflight.outputs.app_version }})" | |
| printf 'Assets:\n- %s\n' "${upload[@]}" | |
| gh release create "$rid" "${upload[@]}" \ | |
| --repo "${{ github.repository }}" \ | |
| --title "$rid" \ | |
| --notes "Windows build on GitHub Actions (workflow run ${{ github.run_id }}). Assets: NSIS installer, latest.yml, portable zip (in-app quick update), optional zip-latest.yml." | |
| release_url="https://github.com/${{ github.repository }}/releases/tag/$rid" | |
| echo "release_url=$release_url" >> "$GITHUB_OUTPUT" | |
| - name: Notify app webhook | |
| if: steps.exists.outputs.exists != 'true' | |
| env: | |
| RELEASE_WEBHOOK_URL: ${{ secrets.RELEASE_WEBHOOK_URL }} | |
| VERCEL_PROJECT_PRODUCTION_URL: ${{ vars.VERCEL_PROJECT_PRODUCTION_URL }} | |
| VERCEL_PROJECT_PRODUCTION_URL_SECRET: ${{ secrets.VERCEL_PROJECT_PRODUCTION_URL }} | |
| WEBHOOK_TOKEN: ${{ secrets.RELEASE_WEBHOOK_TOKEN }} | |
| RELEASE_ID: ${{ steps.artifact.outputs.release_id }} | |
| RELEASE_URL: ${{ steps.create.outputs.release_url }} | |
| EXE_NAME: ${{ steps.artifact.outputs.exe_name }} | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| set -euo pipefail | |
| webhook_url="${RELEASE_WEBHOOK_URL:-}" | |
| if [[ -z "$webhook_url" ]]; then | |
| if [[ -n "${VERCEL_PROJECT_PRODUCTION_URL:-}" ]]; then | |
| webhook_url="https://${VERCEL_PROJECT_PRODUCTION_URL}/api/releases" | |
| elif [[ -n "${VERCEL_PROJECT_PRODUCTION_URL_SECRET:-}" ]]; then | |
| webhook_url="https://${VERCEL_PROJECT_PRODUCTION_URL_SECRET}/api/releases" | |
| fi | |
| fi | |
| if [[ -z "$webhook_url" ]]; then | |
| echo "No webhook URL configured; skipping." | |
| exit 0 | |
| fi | |
| published_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| export PUBLISHED_AT="$published_at" | |
| asset_url="${RELEASE_URL/\/tag\//\/download\/}/${EXE_NAME}" | |
| export ASSET_URL="$asset_url" | |
| payload="$(python3 -c "import json, os; print(json.dumps({'release_id': os.environ['RELEASE_ID'], 'published_at': os.environ['PUBLISHED_AT'], 'platform': 'windows', 'assets': [{'name': os.environ['EXE_NAME'], 'url': os.environ['ASSET_URL']}], 'github_release_url': os.environ['RELEASE_URL']}))")" | |
| if [[ -n "${WEBHOOK_TOKEN:-}" ]]; then | |
| curl --fail --show-error --silent \ | |
| -X POST "$webhook_url" \ | |
| -H "Content-Type: application/json" \ | |
| -H "x-release-token: $WEBHOOK_TOKEN" \ | |
| --data "$payload" | |
| else | |
| curl --fail --show-error --silent \ | |
| -X POST "$webhook_url" \ | |
| -H "Content-Type: application/json" \ | |
| --data "$payload" | |
| fi | |
| - name: Summary | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| if [[ "${{ steps.exists.outputs.exists }}" == "true" ]]; then | |
| echo "Skipped: release already exists for ${{ steps.artifact.outputs.release_id }}." | |
| else | |
| echo "Published ${{ steps.artifact.outputs.release_id }} (${{ steps.artifact.outputs.exe_path }})" | |
| fi |