Skip to content

Commit ebacd6f

Browse files
committed
build(windows): per-build releases layout and Forge under windows/
- Add build-layout.cjs for RELEASE_BUILD_ID, BUILD_STAMP, releases/builder/<id>/eb-output staging, and fixed dev/ folder name for non-installer artifacts.\n- run-win-electron-builder.cjs sets env and runs cleanup; build-with-progress.cjs passes HSP_EB_OUTPUT and directories.output to electron-builder.\n- cleanup.cjs reads HSP_EB_OUTPUT, moves leftover staging into dev/, drops eb-output after organize.\n- Relocate Forge config and make-with-stamp to app/windows/forge/; package.json config.forge points there; add forge-cleanup.cjs for parity with builder layout.\n- NSIS runAfterFinish true (package.json + Forge maker) so the app can launch after install.\n- CI workflows read installer from releases/builder/<id>/ and zip/yml from dev/; refresh docs for paths and commands. Made-with: Cursor
1 parent 64a566f commit ebacd6f

18 files changed

Lines changed: 660 additions & 202 deletions
Lines changed: 103 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build Windows installer with Electron Forge on CI, publish GitHub Release, notify /api/releases.
2-
name: Windows release (Forge CI build)
2+
name: forge .exe build
33

44
on:
55
workflow_dispatch:
@@ -45,108 +45,58 @@ jobs:
4545
4646
- name: Build Forge Windows artifacts
4747
env:
48-
# Принудительно отключаем публикацию для electron-builder внутри Forge
4948
ELECTRON_PUBLISH: never
50-
# Дополнительная заглушка: передаем пустой конфиг публикаций в формате JSON
5149
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5250
run: npm run make:win:forge
5351

54-
- name: Locate Forge artifacts and normalize names
52+
# forge-cleanup writes releases/forge/<build_id>_forge/ (installer at root; zip + yml in dev/).
53+
# Stage flat copies under releases/electron/<build_id>_forge/ for GitHub upload.
54+
- name: Locate Forge artifacts and stage under releases/electron
5555
id: artifact
5656
run: |
5757
set -euo pipefail
58-
shopt -s globstar nullglob
59-
6058
rid="${{ steps.meta.outputs.release_id }}"
61-
stage_dir="releases/$rid"
59+
src_dir="releases/forge/$rid"
60+
dev_dir="$src_dir/dev"
61+
stage_dir="releases/electron/$rid"
6262
mkdir -p "$stage_dir"
6363
64-
forge_root="releases/forge/artifacts"
65-
if [[ ! -d "$forge_root" ]]; then
66-
echo "Forge output folder not found: $forge_root"
67-
exit 1
68-
fi
69-
70-
# Collect outputs under Forge make folders.
71-
exes=( "$forge_root"/**/make/**/*.exe )
72-
zips=( "$forge_root"/**/make/**/*.zip )
73-
ymls=( "$forge_root"/**/make/**/latest.yml )
74-
zip_ymls=( "$forge_root"/**/make/**/zip-latest.yml )
75-
76-
# Pick installer exe (prefer names that look like setup/installer).
77-
exe=""
78-
for f in "${exes[@]}"; do
79-
b="$(basename "$f")"
80-
lb="${b,,}"
81-
if [[ "$lb" == *setup*.exe || "$lb" == *installer*.exe ]]; then
82-
exe="$f"
83-
break
84-
fi
85-
done
86-
if [[ -z "$exe" && ${#exes[@]} -gt 0 ]]; then
87-
exe="${exes[0]}"
88-
fi
64+
exe="$(ls -1 "$src_dir"/HyperlinksSpaceAppInstaller_*.exe 2>/dev/null | head -n 1 || true)"
8965
if [[ -z "$exe" || ! -f "$exe" ]]; then
90-
echo "No Forge Windows installer .exe found under $forge_root/**/make/"
66+
echo "Expected timestamped installer in $src_dir (forge-cleanup; set RELEASE_BUILD_ID=$rid)."
67+
find releases -maxdepth 6 -type f 2>/dev/null || true
9168
exit 1
9269
fi
93-
94-
# Prefer portable zip if present.
95-
zip=""
96-
if [[ ${#zips[@]} -gt 0 ]]; then
97-
zip="${zips[0]}"
98-
fi
99-
100-
yml=""
101-
if [[ ${#ymls[@]} -gt 0 ]]; then
102-
yml="${ymls[0]}"
70+
zip="$(ls -1 "$dev_dir"/HyperlinksSpaceApp_*.zip 2>/dev/null | head -n 1 || true)"
71+
if [[ -z "$zip" || ! -f "$zip" ]]; then
72+
echo "Portable zip required in $dev_dir (Forge zip maker + forge-cleanup)."
73+
find "$dev_dir" -maxdepth 1 -type f 2>/dev/null || true
74+
exit 1
10375
fi
104-
105-
zip_yml=""
106-
if [[ ${#zip_ymls[@]} -gt 0 ]]; then
107-
zip_yml="${zip_ymls[0]}"
76+
yml="$dev_dir/latest.yml"
77+
zip_yml="$dev_dir/zip-latest.yml"
78+
if [[ ! -f "$yml" ]]; then
79+
echo "latest.yml missing next to installer — electron-updater needs it on the GitHub Release."
80+
exit 1
10881
fi
109-
110-
stamp="${rid#build_}"
111-
stamp="${stamp%_forge}"
112-
113-
exe_name="HyperlinksSpaceAppInstaller_${stamp}_forge.exe"
82+
exe_name="$(basename "$exe")"
83+
zip_name="$(basename "$zip")"
11484
exe_out="$stage_dir/$exe_name"
85+
zip_out="$stage_dir/$zip_name"
86+
yml_out="$stage_dir/latest.yml"
87+
zip_yml_out="$stage_dir/zip-latest.yml"
11588
cp "$exe" "$exe_out"
116-
117-
zip_name=""
118-
zip_out=""
119-
if [[ -n "$zip" && -f "$zip" ]]; then
120-
base_zip="$(basename "$zip")"
121-
zip_name="${base_zip%.zip}_forge.zip"
122-
zip_out="$stage_dir/$zip_name"
123-
cp "$zip" "$zip_out"
124-
fi
125-
126-
yml_name=""
127-
yml_out=""
128-
if [[ -n "$yml" && -f "$yml" ]]; then
129-
yml_name="latest_forge.yml"
130-
yml_out="$stage_dir/$yml_name"
131-
cp "$yml" "$yml_out"
132-
fi
133-
134-
zip_yml_name=""
135-
zip_yml_out=""
136-
if [[ -n "$zip_yml" && -f "$zip_yml" ]]; then
137-
zip_yml_name="zip-latest_forge.yml"
138-
zip_yml_out="$stage_dir/$zip_yml_name"
89+
cp "$zip" "$zip_out"
90+
cp "$yml" "$yml_out"
91+
if [[ -f "$zip_yml" ]]; then
13992
cp "$zip_yml" "$zip_yml_out"
14093
fi
141-
14294
echo "exe_path=$exe_out" >> "$GITHUB_OUTPUT"
14395
echo "exe_name=$exe_name" >> "$GITHUB_OUTPUT"
14496
echo "zip_path=$zip_out" >> "$GITHUB_OUTPUT"
14597
echo "zip_name=$zip_name" >> "$GITHUB_OUTPUT"
14698
echo "yml_path=$yml_out" >> "$GITHUB_OUTPUT"
147-
echo "yml_name=$yml_name" >> "$GITHUB_OUTPUT"
14899
echo "zip_yml_path=$zip_yml_out" >> "$GITHUB_OUTPUT"
149-
echo "zip_yml_name=$zip_yml_name" >> "$GITHUB_OUTPUT"
150100
echo "release_id=$rid" >> "$GITHUB_OUTPUT"
151101
152102
- name: Check if GitHub release already exists
@@ -163,6 +113,56 @@ jobs:
163113
echo "exists=false" >> "$GITHUB_OUTPUT"
164114
fi
165115
116+
- name: Preflight checks and artifact summary
117+
if: steps.exists.outputs.exists != 'true'
118+
id: preflight
119+
run: |
120+
set -euo pipefail
121+
rid="${{ steps.artifact.outputs.release_id }}"
122+
exe="${{ steps.artifact.outputs.exe_path }}"
123+
zip="${{ steps.artifact.outputs.zip_path }}"
124+
yml="${{ steps.artifact.outputs.yml_path }}"
125+
zip_yml="${{ steps.artifact.outputs.zip_yml_path }}"
126+
stage_dir="releases/electron/$rid"
127+
128+
[[ -d "$stage_dir" ]] || { echo "Stage dir missing: $stage_dir"; exit 1; }
129+
[[ -f "$exe" ]] || { echo "Installer missing: $exe"; exit 1; }
130+
[[ -f "$zip" ]] || { echo "Portable zip missing: $zip"; exit 1; }
131+
[[ -f "$yml" ]] || { echo "latest.yml missing: $yml"; exit 1; }
132+
133+
exe_name="$(basename "$exe")"
134+
zip_name="$(basename "$zip")"
135+
yml_name="$(basename "$yml")"
136+
137+
[[ "$exe_name" == HyperlinksSpaceAppInstaller_*.exe ]] || {
138+
echo "Unexpected installer name: $exe_name"; exit 1;
139+
}
140+
[[ "$zip_name" == HyperlinksSpaceApp_*.zip ]] || {
141+
echo "Unexpected zip name: $zip_name"; exit 1;
142+
}
143+
[[ "$yml_name" == "latest.yml" ]] || {
144+
echo "Unexpected yml name: $yml_name"; exit 1;
145+
}
146+
147+
app_version="$(echo "$zip_name" | sed -E 's/^HyperlinksSpaceApp_(.+)\.zip$/\1/')"
148+
if [[ -z "$app_version" || "$app_version" == "$zip_name" ]]; then
149+
app_version="unknown"
150+
fi
151+
echo "app_version=$app_version" >> "$GITHUB_OUTPUT"
152+
153+
echo "Preflight OK (Forge):"
154+
echo " release_id: $rid"
155+
echo " app_version: $app_version"
156+
echo " stage_dir: $stage_dir"
157+
echo " installer: $exe"
158+
echo " portable_zip: $zip"
159+
echo " latest_yml: $yml"
160+
if [[ -n "$zip_yml" && -f "$zip_yml" ]]; then
161+
echo " zip_latest_yml: $zip_yml"
162+
else
163+
echo " zip_latest_yml: (missing, optional)"
164+
fi
165+
166166
- name: Create GitHub release and upload Forge artifacts
167167
if: steps.exists.outputs.exists != 'true'
168168
id: create
@@ -175,27 +175,18 @@ jobs:
175175
zip="${{ steps.artifact.outputs.zip_path }}"
176176
yml="${{ steps.artifact.outputs.yml_path }}"
177177
zip_yml="${{ steps.artifact.outputs.zip_yml_path }}"
178-
179-
if [[ ! -f "$exe" ]]; then
180-
echo "Installer artifact missing: $exe"
181-
exit 1
182-
fi
183-
184-
upload=( "$exe" )
185-
if [[ -n "$yml" && -f "$yml" ]]; then
186-
upload+=( "$yml" )
187-
fi
188-
if [[ -n "$zip" && -f "$zip" ]]; then
189-
upload+=( "$zip" )
190-
fi
191-
if [[ -n "$zip_yml" && -f "$zip_yml" ]]; then
178+
upload=( "$exe" "$yml" "$zip" )
179+
if [[ -f "$zip_yml" ]]; then
192180
upload+=( "$zip_yml" )
181+
else
182+
echo "Warning: zip-latest.yml missing (forge-cleanup should generate it when zip exists)."
193183
fi
194-
184+
echo "Publishing release $rid (app_version=${{ steps.preflight.outputs.app_version }})"
185+
printf 'Assets:\n- %s\n' "${upload[@]}"
195186
gh release create "$rid" "${upload[@]}" \
196187
--repo "${{ github.repository }}" \
197188
--title "$rid" \
198-
--notes "Windows Forge build on GitHub Actions. Assets are Forge artifacts normalized with _forge suffix."
189+
--notes "Windows Forge build on GitHub Actions (workflow run ${{ github.run_id }}). Same asset names as electron-builder; tag ends with _forge."
199190
release_url="https://github.com/${{ github.repository }}/releases/tag/$rid"
200191
echo "release_url=$release_url" >> "$GITHUB_OUTPUT"
201192
@@ -230,7 +221,23 @@ jobs:
230221
export ASSET_URL="$asset_url"
231222
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']}))")"
232223
if [[ -n "${WEBHOOK_TOKEN:-}" ]]; then
233-
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${WEBHOOK_TOKEN}" -d "$payload" "$webhook_url"
224+
curl --fail --show-error --silent \
225+
-X POST "$webhook_url" \
226+
-H "Content-Type: application/json" \
227+
-H "x-release-token: $WEBHOOK_TOKEN" \
228+
--data "$payload"
229+
else
230+
curl --fail --show-error --silent \
231+
-X POST "$webhook_url" \
232+
-H "Content-Type: application/json" \
233+
--data "$payload"
234+
fi
235+
236+
- name: Summary
237+
working-directory: ${{ github.workspace }}
238+
run: |
239+
if [[ "${{ steps.exists.outputs.exists }}" == "true" ]]; then
240+
echo "Skipped: release already exists for ${{ steps.artifact.outputs.release_id }}."
234241
else
235-
curl -X POST -H "Content-Type: application/json" -d "$payload" "$webhook_url"
242+
echo "Published ${{ steps.artifact.outputs.release_id }} (${{ steps.artifact.outputs.exe_path }})"
236243
fi

.github/workflows/releases-from-folder.yml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build Windows installer on CI (no .exe committed to git), publish GitHub Release, notify /api/releases.
2-
name: Windows release (CI build)
2+
name: .exe build
33

44
on:
55
workflow_dispatch:
@@ -47,30 +47,31 @@ jobs:
4747
- name: Build Windows installer
4848
run: npm run build:win
4949

50-
# cleanup.cjs writes installer + latest.yml + portable zip + zip-latest.yml under releases/<build_id>/.
51-
# Then we stage them under releases/electron/<build_id>/ to keep release technologies separated.
50+
# cleanup.cjs writes releases/builder/<build_id>/ (installer at root; zip + yml in dev/).
51+
# Stage flat copies under releases/electron/<build_id>/ for GitHub upload.
5252
- name: Locate electron-builder artifacts and stage under releases/electron
5353
id: artifact
5454
run: |
5555
set -euo pipefail
5656
rid="${{ steps.meta.outputs.release_id }}"
57-
src_dir="releases/$rid"
57+
src_dir="releases/builder/$rid"
58+
dev_dir="$src_dir/dev"
5859
stage_dir="releases/electron/$rid"
5960
mkdir -p "$stage_dir"
6061
exe="$(ls -1 "$src_dir"/HyperlinksSpaceAppInstaller_*.exe 2>/dev/null | head -n 1 || true)"
6162
if [[ -z "$exe" || ! -f "$exe" ]]; then
6263
echo "Expected timestamped installer in $src_dir (set RELEASE_BUILD_ID=$rid)."
63-
find releases -maxdepth 5 -type f 2>/dev/null || true
64+
find releases -maxdepth 6 -type f 2>/dev/null || true
6465
exit 1
6566
fi
66-
zip="$(ls -1 "$src_dir"/HyperlinksSpaceApp_*.zip 2>/dev/null | head -n 1 || true)"
67+
zip="$(ls -1 "$dev_dir"/HyperlinksSpaceApp_*.zip 2>/dev/null | head -n 1 || true)"
6768
if [[ -z "$zip" || ! -f "$zip" ]]; then
68-
echo "Portable zip HyperlinksSpaceApp_<version>.zip is required for in-app updates (electron-builder zip target + cleanup)."
69-
find "$src_dir" -maxdepth 1 -type f 2>/dev/null || true
69+
echo "Portable zip HyperlinksSpaceApp_<version>.zip is required in $dev_dir (electron-builder zip target + cleanup)."
70+
find "$dev_dir" -maxdepth 1 -type f 2>/dev/null || true
7071
exit 1
7172
fi
72-
yml="$(dirname "$exe")/latest.yml"
73-
zip_yml="$(dirname "$zip")/zip-latest.yml"
73+
yml="$dev_dir/latest.yml"
74+
zip_yml="$dev_dir/zip-latest.yml"
7475
if [[ ! -f "$yml" ]]; then
7576
echo "latest.yml missing next to installer — electron-updater needs it on the GitHub Release."
7677
exit 1

app/docs/build_and_install.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ This doc describes the Windows Electron build and install flow and how to speed
55
Exe creating example:
66

77
```
8-
cd /d C:\1\1\1\1\1\HyperlinksSpaceBot\app
8+
cd /d C:\1\1\1\1\1\HyperlinksSpaceProgram\app
99
npm run build:win:verbose
1010
```
1111

1212
Exe bash run example:
1313

1414
```
15-
powershell -NoProfile -Command "Start-Process -FilePath 'C:/1/1/1/1/1/HyperlinksSpaceBot/app/releases/build_03252026_1449/HyperlinksSpaceAppInstaller.exe'"
15+
powershell -NoProfile -Command "Start-Process -FilePath 'C:/1/1/1/1/1/HyperlinksSpaceBot/app/releases/builder/build_03252026_1449/HyperlinksSpaceAppInstaller_03252026_1449.exe'"
1616
```
1717

1818
---
@@ -25,9 +25,9 @@ powershell -NoProfile -Command "Start-Process -FilePath 'C:/1/1/1/1/1/Hyperlinks
2525

2626
1. **Expo web export**`npm run build``expo export -p web`. Metro bundles the app and writes static files to `dist/`. Usually the slowest step (tens of seconds).
2727
2. **Electron pack**`electron-builder --win`. Rebuilds native deps (if any), packages the app, builds the NSIS installer. Downloads (Electron, NSIS, winCodeSign) are cached after first run.
28-
3. **Clean**`node windows/cleanup.cjs`. Moves artifacts into `releases/build_MMDDYYYY_HHMM/` (installer at root, optional files in `dev/`).
28+
3. **Clean**`node windows/cleanup.cjs`. Moves artifacts into `releases/builder/build_MMDDYYYY_HHMM/` (installer only at root; zip, yml, unpacked, and other artifacts under `dev/`).
2929

30-
**Output:** `releases/build_<date>_<time>/HyperlinksSpaceAppInstaller.exe` and optionally `dev/` (win-unpacked, blockmap, builder-debug.yml, builder-effective-config.yaml).
30+
**Output:** `releases/builder/build_<date>_<time>/HyperlinksSpaceAppInstaller_<stamp>.exe` at root and `dev/` (portable zip, latest.yml, zip-latest.yml, win-unpacked, blockmap, builder-debug.yml, builder-effective-config.yaml).
3131

3232
### How to make the build faster
3333

@@ -72,7 +72,7 @@ User runs **`HyperlinksSpaceAppInstaller.exe`**. NSIS extracts the app (e.g. to
7272
| **Auto-updates** | Add `electron-updater` (or similar) and serve updates over HTTPS so users get patches without reinstalling. |
7373
| **CI/CD** | In CI, cache `node_modules`, `.expo`, Metro cache, and `electron-builder` cache to make repeated builds much faster. |
7474
| **Developer Mode (Windows)** | If you build on Windows and use exe editing (icon, etc.), Developer Mode avoids symlink errors during the winCodeSign step. |
75-
| **Structured releases** | `windows/cleanup.cjs` puts each build in `releases/build_<date>_<time>/` with the installer at root and optional artifacts in `dev/`. |
75+
| **Structured releases** | `windows/cleanup.cjs` puts each build in `releases/builder/build_<date>_<time>/` with the installer at root and all other artifacts in `dev/`. |
7676

7777
---
7878

@@ -89,7 +89,7 @@ User runs **`HyperlinksSpaceAppInstaller.exe`**. NSIS extracts the app (e.g. to
8989

9090
## 5. File layout after build
9191

92-
- **After `build:win` or `pack:win`:** `app/releases/build_MMDDYYYY_HHMM/HyperlinksSpaceAppInstaller.exe` and `app/releases/build_MMDDYYYY_HHMM/dev/` (win-unpacked, blockmap, builder-debug.yml, builder-effective-config.yaml).
92+
- **After `build:win` or `pack:win`:** `app/releases/builder/build_MMDDYYYY_HHMM/HyperlinksSpaceAppInstaller_<stamp>.exe` and `app/releases/builder/build_MMDDYYYY_HHMM/dev/` (portable zip, yml, win-unpacked, blockmap, builder-debug.yml, builder-effective-config.yaml).
9393
- **After `build:win:dir`:** `app/release/win-unpacked/` (run `app.exe` from there).
9494
- **Build inputs:** `dist/` (Expo export), `windows/` (build.cjs, app-shell.html, preload-log.cjs), `assets/icon.ico`, and files listed under `build.files` in `package.json`.
9595

app/docs/releases.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This document defines the production plan for release detection, deduped GitHub
1212
## Source of Truth
1313

1414
- The **GitHub Release tag** is the release identity (`release_id`), for example `build_03252026_1929`.
15-
- Locally, `windows/cleanup.cjs` moves the built installer to `app/releases/build_MMDDYYYY_HHMM/HyperlinksSpaceAppInstaller.exe`. In CI you can set `RELEASE_BUILD_ID` to match a chosen tag, or leave it unset so the folder name comes from build time.
15+
- Locally, `windows/cleanup.cjs` moves the built installer to `app/releases/builder/build_MMDDYYYY_HHMM/HyperlinksSpaceAppInstaller_<stamp>.exe` (other artifacts under `dev/`). In CI you can set `RELEASE_BUILD_ID` to match a chosen tag, or leave it unset so the folder name comes from build time.
1616

1717
## Workflow Trigger
1818

@@ -34,7 +34,7 @@ on:
3434
## Dedupe Rules (No Duplicate Releases)
3535
3636
1. Run `npm run build:win` (or use optional `RELEASE_BUILD_ID` so the output folder matches the intended tag).
37-
2. Resolve `release_id` from the optional input or from `releases/build_*/HyperlinksSpaceAppInstaller.exe`.
37+
2. Resolve `release_id` from the optional input or from `releases/builder/build_*/HyperlinksSpaceAppInstaller_*.exe`.
3838
3. Check whether GitHub Release/tag already exists for that `release_id`.
3939
4. If it exists:
4040
- Exit successfully (`0`)
@@ -193,7 +193,7 @@ Client behavior:
193193

194194
## Acceptance Criteria
195195

196-
- A new `app/releases/build_.../` folder produces exactly one GitHub Release.
196+
- A new `app/releases/builder/build_.../` folder produces exactly one GitHub Release.
197197
- Re-running workflow for the same `release_id` does not create duplicates.
198198
- Webhook is called only for newly created releases.
199199
- `POST /api/releases` rejects unauthorized requests.

0 commit comments

Comments
 (0)