Skip to content

Commit 2b9be0a

Browse files
committed
Add build.sh and build-release workflow
1 parent 4172a33 commit 2b9be0a

3 files changed

Lines changed: 383 additions & 9 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
name: Build & Release Hopkit
2+
3+
on:
4+
push:
5+
paths:
6+
- 'package.json'
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
check-version:
14+
runs-on: ubuntu-latest
15+
outputs:
16+
version: ${{ steps.get_version.outputs.version }}
17+
build: ${{ steps.compare.outputs.build }}
18+
steps:
19+
- uses: actions/checkout@v5
20+
with:
21+
fetch-depth: 0
22+
23+
- id: get_version
24+
run: |
25+
VERSION=$(python3 -c 'import json,sys;print(json.load(open("package.json"))["version"])')
26+
echo "version=$VERSION" >> $GITHUB_OUTPUT
27+
28+
- id: get_latest
29+
run: |
30+
TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("tag_name",""))')
31+
echo "tag=$TAG" >> $GITHUB_OUTPUT
32+
33+
- id: compare
34+
run: |
35+
VERSION="${{ steps.get_version.outputs.version }}"
36+
TAG="${{ steps.get_latest.outputs.tag }}"
37+
if [ "v$VERSION" = "$TAG" ]; then
38+
echo "build=false" >> $GITHUB_OUTPUT
39+
else
40+
echo "build=true" >> $GITHUB_OUTPUT
41+
fi
42+
43+
build:
44+
needs: check-version
45+
if: needs.check-version.outputs.build == 'true'
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v5
49+
with:
50+
fetch-depth: 0
51+
52+
- name: Cache NW.js and appimagetool
53+
uses: actions/cache@v4
54+
with:
55+
path: |
56+
nwjs-cache
57+
key: nwjs-${{ runner.os }}-${{ hashFiles('package.json') }}-${{ env.NW_VERSION }}
58+
env:
59+
NW_VERSION: v0.108.0
60+
61+
- name: Ensure python3 available
62+
uses: actions/setup-python@v4
63+
with:
64+
python-version: '3.x'
65+
66+
- name: Make build script executable
67+
run: chmod +x ./build.sh
68+
69+
- name: Run build (produces dist/)
70+
env:
71+
NW_VERSION: v0.108.0
72+
run: ./build.sh
73+
74+
- name: Upload dist artifacts
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: dist-artifacts
78+
path: dist/**
79+
80+
release:
81+
needs: build
82+
if: needs.check-version.outputs.build == 'true'
83+
runs-on: ubuntu-latest
84+
steps:
85+
- uses: actions/checkout@v5
86+
with:
87+
fetch-depth: 0
88+
89+
- name: Download artifacts
90+
uses: actions/download-artifact@v4
91+
with:
92+
name: dist-artifacts
93+
path: dist
94+
95+
- name: Read displayName & version
96+
id: meta
97+
run: |
98+
DISPLAY=$(python3 -c 'import json,re;d=json.load(open("package.json"));disp=d.get("displayName", d.get("name","App"));print(re.sub(r"[^A-Za-z0-9._-]","",disp))')
99+
VERSION=$(python3 -c 'import json;print(json.load(open("package.json"))["version"])')
100+
echo "display=$DISPLAY" >> $GITHUB_OUTPUT
101+
echo "version=$VERSION" >> $GITHUB_OUTPUT
102+
103+
- name: Create release
104+
id: create_release
105+
env:
106+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
107+
run: |
108+
VERSION="${{ needs.check-version.outputs.version }}"
109+
TAG="v${VERSION}"
110+
# create release using GitHub CLI if available, otherwise use API via curl+jq
111+
if command -v gh >/dev/null 2>&1; then
112+
gh release create "${TAG}" --title "${TAG}" --notes "Release ${TAG}" || true
113+
else
114+
# create minimal release via API (jq used only if available; fallback to raw JSON)
115+
PAYLOAD=$(printf '{"tag_name":"%s","name":"%s"}' "$TAG" "$TAG")
116+
curl -s -X POST \
117+
-H "Authorization: token ${GITHUB_TOKEN}" \
118+
-H "Content-Type: application/json" \
119+
-d "$PAYLOAD" \
120+
"https://api.github.com/repos/${{ github.repository }}/releases" > /tmp/release.json || true
121+
fi
122+
123+
- name: Upload release assets (named exactly)
124+
env:
125+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126+
run: |
127+
VERSION="${{ needs.check-version.outputs.version }}"
128+
TAG="v${VERSION}"
129+
DISPLAY="${{ steps.meta.outputs.display }}"
130+
# fallback if meta didn't set outputs (defensive)
131+
if [ -z "$DISPLAY" ]; then
132+
DISPLAY=$(python3 -c 'import json,re;d=json.load(open("package.json"));disp=d.get("displayName",d.get("name","App"));print(re.sub(r"[^A-Za-z0-9._-]","",disp))')
133+
fi
134+
135+
# get upload_url for the release
136+
UPLOAD_URL=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "https://api.github.com/repos/${{ github.repository }}/releases/tags/${TAG}" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('upload_url','').split('{')[0])")
137+
if [ -z "$UPLOAD_URL" ]; then
138+
echo "Could not determine upload_url for release ${TAG}"
139+
exit 1
140+
fi
141+
142+
upload() {
143+
local filepath="$1"
144+
local asset_name="$2"
145+
if [ -f "$filepath" ]; then
146+
echo "Uploading $filepath as $asset_name"
147+
curl -s --data-binary @"$filepath" \
148+
-H "Authorization: token ${GITHUB_TOKEN}" \
149+
-H "Content-Type: application/octet-stream" \
150+
"${UPLOAD_URL}?name=$(basename "$asset_name")" > /dev/null || true
151+
else
152+
echo "missing file: $filepath"
153+
fi
154+
}
155+
156+
# Attach mac: upload zipped .app but name the asset as ${DISPLAY}.app
157+
if [ -f "dist/${DISPLAY}.app.zip" ]; then
158+
upload "dist/${DISPLAY}.app.zip" "${DISPLAY}.app"
159+
elif [ -d "dist/${DISPLAY}.app" ]; then
160+
(cd dist && zip -r -q "${DISPLAY}.app.zip" "${DISPLAY}.app")
161+
upload "dist/${DISPLAY}.app.zip" "${DISPLAY}.app"
162+
else
163+
echo "No mac artifact found for ${DISPLAY}"
164+
fi
165+
166+
# Attach Windows single exe if created
167+
if [ -f "dist/${DISPLAY}.exe" ]; then
168+
upload "dist/${DISPLAY}.exe" "${DISPLAY}.exe"
169+
elif [ -f "dist/${DISPLAY}-win-${VERSION}.zip" ]; then
170+
upload "dist/${DISPLAY}-win-${VERSION}.zip" "${DISPLAY}.exe"
171+
else
172+
echo "No windows artifact found for ${DISPLAY}"
173+
fi
174+
175+
# Attach Linux AppImage
176+
if [ -f "dist/${DISPLAY}.appimage" ]; then
177+
upload "dist/${DISPLAY}.appimage" "${DISPLAY}.appimage"
178+
elif [ -f "dist/${DISPLAY}.AppImage" ]; then
179+
upload "dist/${DISPLAY}.AppImage" "${DISPLAY}.appimage"
180+
else
181+
echo "No linux AppImage found for ${DISPLAY}"
182+
fi

build.sh

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# config
5+
NW_VERSION="${NW_VERSION:-v0.108.0}"
6+
ARCH="${ARCH:-x64}"
7+
SRCDIR="${SRCDIR:-src}"
8+
CACHE_DIR="${CACHE_DIR:-./nwjs-cache}"
9+
BUILD_DIR="${BUILD_DIR:-./build}"
10+
DIST_DIR="${DIST_DIR:-./dist}"
11+
APPIMAGETOOL="${CACHE_DIR}/appimagetool-x86_64.AppImage"
12+
13+
mkdir -p "$CACHE_DIR" "$BUILD_DIR" "$DIST_DIR"
14+
15+
# read metadata from package.json
16+
read -r NAME DISPLAY VERSION <<EOF
17+
$(python3 - <<PY
18+
import json,sys,re
19+
d=json.load(open('package.json'))
20+
name = d.get('name','app')
21+
display = d.get('displayName', d.get('name','App'))
22+
version = d.get('version','0.0.0')
23+
# sanitize display for filenames: keep letters, numbers, dash, underscore
24+
display = re.sub(r'[^A-Za-z0-9._-]','',display)
25+
print(name, display, version)
26+
PY
27+
)
28+
EOF
29+
30+
echo "Building ${DISPLAY} (package name: ${NAME}) version ${VERSION}"
31+
echo "NW.js: ${NW_VERSION} arch: ${ARCH}"
32+
33+
# cached download
34+
cached_download() {
35+
local url="$1"; local outname="$2"
36+
local outpath="$CACHE_DIR/$outname"
37+
if [ -f "$outpath" ]; then
38+
echo "Using cached $outname"
39+
else
40+
echo "Downloading $outname"
41+
curl -L -sS -o "$outpath" "$url"
42+
fi
43+
echo "$outpath"
44+
}
45+
46+
# clean previous build
47+
rm -rf "$BUILD_DIR"
48+
mkdir -p "$BUILD_DIR"
49+
50+
# macos
51+
build_macos() {
52+
echo "=== macOS build ==="
53+
local file="nwjs-${NW_VERSION}-osx-${ARCH}.zip"
54+
local url="https://dl.nwjs.io/${NW_VERSION}/${file}"
55+
local zip=$(cached_download "$url" "$file")
56+
57+
tmp=$(mktemp -d)
58+
unzip -q "$zip" -d "$tmp"
59+
NW_APP=$(find "$tmp" -type d -name "nwjs.app" -print -quit || true)
60+
if [ -z "$NW_APP" ]; then
61+
NW_APP=$(find "$tmp" -maxdepth 2 -type d -name "*.app" -print -quit || true)
62+
fi
63+
if [ -z "$NW_APP" ]; then
64+
echo "nwjs.app not found in archive"
65+
rm -rf "$tmp"
66+
return 1
67+
fi
68+
69+
OUT_APP="${BUILD_DIR}/${DISPLAY}.app"
70+
rm -rf "$OUT_APP"
71+
cp -R "$NW_APP" "$OUT_APP"
72+
73+
APP_NW_DIR="${OUT_APP}/Contents/Resources/app.nw"
74+
rm -rf "$APP_NW_DIR"
75+
mkdir -p "$APP_NW_DIR"
76+
cp -R "${SRCDIR}/." "$APP_NW_DIR/"
77+
78+
PLIST="${OUT_APP}/Contents/Info.plist"
79+
if [ -f "icon.icns" ]; then
80+
cp "icon.icns" "${OUT_APP}/Contents/Resources/app.icns" || true
81+
if command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then
82+
/usr/libexec/PlistBuddy -c "Set :CFBundleName ${DISPLAY}" "$PLIST" || true
83+
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${DISPLAY}" "$PLIST" || true
84+
/usr/libexec/PlistBuddy -c "Set :CFBundleIconFile app.icns" "$PLIST" || true
85+
fi
86+
fi
87+
88+
mkdir -p "$DIST_DIR"
89+
cp -R "$OUT_APP" "$DIST_DIR/${DISPLAY}.app"
90+
(cd "$BUILD_DIR" && zip -r -q "../${DIST_DIR}/${DISPLAY}.app.zip" "$(basename "$OUT_APP")")
91+
92+
rm -rf "$tmp"
93+
echo "mac: produced ${DIST_DIR}/${DISPLAY}.app (bundle) and ${DIST_DIR}/${DISPLAY}.app.zip"
94+
}
95+
96+
# windows
97+
build_windows() {
98+
echo "=== Windows build ==="
99+
local file="nwjs-${NW_VERSION}-win-${ARCH}.zip"
100+
local url="https://dl.nwjs.io/${NW_VERSION}/${file}"
101+
local zip=$(cached_download "$url" "$file")
102+
103+
tmp=$(mktemp -d)
104+
unzip -q "$zip" -d "$tmp"
105+
106+
TOPDIR=$(find "$tmp" -maxdepth 1 -type d -name "nwjs*" -print -quit || true)
107+
if [ -z "$TOPDIR" ]; then TOPDIR="$tmp"; fi
108+
109+
OUT_DIR="${BUILD_DIR}/${DISPLAY}-win"
110+
rm -rf "$OUT_DIR"
111+
mkdir -p "$OUT_DIR"
112+
cp -R "$TOPDIR/"* "$OUT_DIR/"
113+
114+
mkdir -p "${OUT_DIR}/${NAME}-files"
115+
cp -R "${SRCDIR}/." "${OUT_DIR}/${NAME}-files/"
116+
117+
PKGZIP="${BUILD_DIR}/package.nw"
118+
(cd "${SRCDIR}" && zip -r -q "../${PKGZIP}" .)
119+
120+
NW_EXE="$(find "$OUT_DIR" -maxdepth 1 -type f -iname 'nw.exe' -print -quit || true)"
121+
if [ -z "$NW_EXE" ]; then
122+
echo "nw.exe not found in extracted runtime"
123+
else
124+
DIST_EXE="${DIST_DIR}/${DISPLAY}.exe"
125+
if command -v cat >/dev/null 2>&1; then
126+
cat "$NW_EXE" "$PKGZIP" > "${DIST_EXE}"
127+
chmod +x "${DIST_EXE}" || true
128+
echo "windows: produced single-file ${DIST_EXE} (note: some runtime files may still be required alongside this exe)"
129+
else
130+
echo "cat not available; skipping single-file exe creation"
131+
fi
132+
fi
133+
134+
mkdir -p "$DIST_DIR"
135+
(cd "$BUILD_DIR" && zip -r -q "../${DIST_DIR}/${DISPLAY}-win-${VERSION}.zip" "$(basename "$OUT_DIR")")
136+
137+
rm -rf "$tmp" "$PKGZIP"
138+
}
139+
140+
# linux
141+
build_linux() {
142+
echo "=== Linux build ==="
143+
local file="nwjs-${NW_VERSION}-linux-${ARCH}.zip"
144+
local url="https://dl.nwjs.io/${NW_VERSION}/${file}"
145+
local zip=$(cached_download "$url" "$file")
146+
147+
tmp=$(mktemp -d)
148+
unzip -q "$zip" -d "$tmp"
149+
TOPDIR=$(find "$tmp" -maxdepth 1 -type d -name "nwjs*" -print -quit || true)
150+
if [ -z "$TOPDIR" ]; then TOPDIR="$tmp"; fi
151+
152+
APPDIR="${BUILD_DIR}/${DISPLAY}.AppDir"
153+
rm -rf "$APPDIR"
154+
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/icons/hicolor/256x256/apps"
155+
156+
cp -R "$TOPDIR/"* "$APPDIR/"
157+
158+
mkdir -p "${APPDIR}/usr/share/${NAME}"
159+
cp -R "${SRCDIR}/." "${APPDIR}/usr/share/${NAME}/"
160+
161+
cat > "${APPDIR}/AppRun" <<AR
162+
#!/usr/bin/env bash
163+
HERE="\$(dirname "\$(readlink -f "\${0}")")"
164+
# run the nw binary from inside the AppDir; prefer ./nw or usr/bin/nw
165+
if [ -x "\$HERE/nw" ]; then
166+
exec "\$HERE/nw" "\$HERE/usr/share/${NAME}" "\$@"
167+
elif [ -x "\$HERE/usr/bin/nw" ]; then
168+
exec "\$HERE/usr/bin/nw" "\$HERE/usr/share/${NAME}" "\$@"
169+
else
170+
echo "nw binary not found in AppImage"
171+
exit 1
172+
fi
173+
AR
174+
chmod +x "${APPDIR}/AppRun"
175+
176+
if [ -f "icon.png" ]; then
177+
cp "icon.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/${DISPLAY}.png" || true
178+
fi
179+
180+
if [ ! -f "$APPIMAGETOOL" ]; then
181+
echo "Downloading appimagetool"
182+
curl -L -sS -o "$APPIMAGETOOL" "https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-x86_64.AppImage"
183+
chmod +x "$APPIMAGETOOL"
184+
else
185+
echo "Using cached appimagetool"
186+
fi
187+
188+
(cd "$BUILD_DIR" && "$APPIMAGETOOL" "${APPDIR}" "${DIST_DIR}/${DISPLAY}.AppImage")
189+
chmod +x "${DIST_DIR}/${DISPLAY}.AppImage"
190+
echo "linux: produced ${DIST_DIR}/${DISPLAY}.AppImage"
191+
rm -rf "$tmp"
192+
}
193+
194+
# run builds
195+
build_macos || echo "mac build failed (continuing)"
196+
build_windows || echo "windows build failed (continuing)"
197+
build_linux || echo "linux build failed (continuing)"
198+
199+
echo "Artifacts in ${DIST_DIR}:"
200+
ls -la "$DIST_DIR" || true

0 commit comments

Comments
 (0)