Skip to content

Commit bf93753

Browse files
committed
Add standalone executable builds with GitHub Actions
esbuild bundles server.js into a single file. Node.js SEA packages it as a standalone executable for Windows, macOS, and Linux. GitHub Actions workflow builds all three on each release. Landing page download buttons detect the user OS and link directly to the correct zip.
1 parent 59fc0e8 commit bf93753

7 files changed

Lines changed: 224 additions & 11 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Build Release Executables
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
build:
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
include:
14+
- os: windows-latest
15+
name: windows-x64
16+
exe: gitdock.exe
17+
script: pwsh -File scripts/build-sea.ps1
18+
start_script: start.ps1
19+
- os: macos-latest
20+
name: macos-x64
21+
exe: gitdock
22+
script: chmod +x scripts/build-sea.sh && ./scripts/build-sea.sh
23+
start_script: start.sh
24+
- os: ubuntu-latest
25+
name: linux-x64
26+
exe: gitdock
27+
script: chmod +x scripts/build-sea.sh && ./scripts/build-sea.sh
28+
start_script: start.sh
29+
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: "22"
38+
cache: "npm"
39+
40+
- name: Install dependencies
41+
run: npm install
42+
43+
- name: Build SEA executable
44+
run: ${{ matrix.script }}
45+
46+
- name: Prepare release folder (Windows)
47+
if: matrix.os == 'windows-latest'
48+
shell: pwsh
49+
run: |
50+
New-Item -ItemType Directory -Force -Path release | Out-Null
51+
Copy-Item "dist\${{ matrix.exe }}" release\
52+
Copy-Item dashboard.html, workspace-setup.html, ${{ matrix.start_script }} release\
53+
54+
- name: Prepare release folder (Unix)
55+
if: matrix.os != 'windows-latest'
56+
shell: bash
57+
run: |
58+
mkdir -p release
59+
cp "dist/${{ matrix.exe }}" release/
60+
cp dashboard.html workspace-setup.html ${{ matrix.start_script }} release/
61+
62+
- name: Create zip (Windows)
63+
if: matrix.os == 'windows-latest'
64+
shell: pwsh
65+
run: |
66+
Compress-Archive -Path release\* -DestinationPath gitdock-${{ matrix.name }}.zip -Force
67+
68+
- name: Create zip (Unix)
69+
if: matrix.os != 'windows-latest'
70+
shell: bash
71+
run: |
72+
cd release && zip -r ../gitdock-${{ matrix.name }}.zip . && cd ..
73+
74+
- name: Upload artifact
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: gitdock-${{ matrix.name }}
78+
path: gitdock-${{ matrix.name }}.zip
79+
80+
upload-release:
81+
needs: build
82+
runs-on: ubuntu-latest
83+
permissions:
84+
contents: write
85+
steps:
86+
- name: Checkout
87+
uses: actions/checkout@v4
88+
89+
- name: Download all artifacts
90+
uses: actions/download-artifact@v4
91+
with:
92+
path: artifacts
93+
94+
- name: Upload to Release
95+
env:
96+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97+
run: |
98+
find artifacts -name "*.zip" -type f | while read f; do
99+
echo "Uploading $f"
100+
gh release upload "${{ github.event.release.tag_name }}" "$f" --clobber
101+
done

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitdock",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Local dashboard for managing multiple GitHub accounts",
55
"license": "Apache-2.0",
66
"repository": {
@@ -10,10 +10,15 @@
1010
"homepage": "https://gitdock.dev",
1111
"scripts": {
1212
"start": "node server.js",
13-
"dev": "node --watch server.js"
13+
"dev": "node --watch server.js",
14+
"build:bundle": "esbuild server.js --bundle --platform=node --outfile=dist/server.bundle.js --external:child_process --external:fs --external:path --external:os --external:https --external:http --external:crypto --external:net --external:tls --external:url --external:stream --external:zlib --external:events --external:util --external:querystring --external:buffer"
1415
},
1516
"bin": "server.js",
1617
"dependencies": {
1718
"express": "^4.21.0"
19+
},
20+
"devDependencies": {
21+
"esbuild": "^0.24.0",
22+
"postject": "^1.0.0-alpha.4"
1823
}
1924
}

scripts/build-sea.ps1

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# =============================================================================
2+
# build-sea.ps1 - Build GitDock SEA executable (Windows)
3+
# =============================================================================
4+
# Requires: Node.js 22 LTS, npm install (esbuild, postject)
5+
# Run from repo root.
6+
# =============================================================================
7+
8+
$ErrorActionPreference = "Stop"
9+
$RootDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
10+
Set-Location $RootDir
11+
12+
Write-Host " [1/4] Bundling with esbuild..." -ForegroundColor Cyan
13+
npm run build:bundle
14+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
15+
16+
Write-Host " [2/4] Generating SEA blob..." -ForegroundColor Cyan
17+
cmd /c "node --experimental-sea-config sea-config.json 2>nul"
18+
$blobPath = Join-Path "dist" "gitdock-sea-prep.blob"
19+
if (-not (Test-Path $blobPath)) {
20+
Write-Host " SEA blob was not created." -ForegroundColor Red
21+
exit 1
22+
}
23+
24+
Write-Host " [3/4] Copying Node binary and injecting blob..." -ForegroundColor Cyan
25+
$NodePath = (Get-Command node).Source
26+
$ExeOut = "dist\gitdock.exe"
27+
Copy-Item -Path $NodePath -Destination $ExeOut -Force
28+
npx postject $ExeOut NODE_SEA_BLOB dist\gitdock-sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
29+
if ($LASTEXITCODE -ne 0) {
30+
Write-Host " postject failed. Try: npm install postject --save-dev" -ForegroundColor Red
31+
exit $LASTEXITCODE
32+
}
33+
34+
Write-Host " [4/4] Done. Executable: $ExeOut" -ForegroundColor Green
35+
Write-Host " Copy dashboard.html and workspace-setup.html to dist/ and run gitdock.exe from that folder." -ForegroundColor Gray

scripts/build-sea.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# build-sea.sh - Build GitDock SEA executable (macOS / Linux)
4+
# =============================================================================
5+
# Requires: Node.js 22 LTS, npm install (esbuild, postject)
6+
# Run from repo root: ./scripts/build-sea.sh
7+
# =============================================================================
8+
9+
set -e
10+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
11+
cd "$ROOT_DIR"
12+
13+
echo " [1/5] Bundling with esbuild..."
14+
npm run build:bundle
15+
16+
echo " [2/5] Generating SEA blob..."
17+
node --experimental-sea-config sea-config.json 2>/dev/null || true
18+
if [ ! -f "dist/gitdock-sea-prep.blob" ]; then
19+
echo " SEA blob was not created."
20+
exit 1
21+
fi
22+
23+
echo " [3/5] Copying Node binary..."
24+
NODE_PATH="$(command -v node)"
25+
EXE_OUT="dist/gitdock"
26+
cp "$NODE_PATH" "$EXE_OUT"
27+
28+
if [ "$(uname -s)" = "Darwin" ]; then
29+
echo " [4/5] Removing macOS signature..."
30+
codesign --remove-signature "$EXE_OUT" 2>/dev/null || true
31+
fi
32+
33+
echo " [5/5] Injecting SEA blob..."
34+
npx postject "$EXE_OUT" NODE_SEA_BLOB dist/gitdock-sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
35+
36+
if [ "$(uname -s)" = "Darwin" ]; then
37+
echo " Re-signing..."
38+
codesign --sign - "$EXE_OUT"
39+
fi
40+
41+
echo " Done. Executable: $EXE_OUT"
42+
echo " Copy dashboard.html and workspace-setup.html to dist/ and run ./gitdock from that folder."

sea-config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"main": "dist/server.bundle.js",
3+
"output": "dist/gitdock-sea-prep.blob",
4+
"disableExperimentalSEAWarning": true
5+
}

server.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ const PORT = parseInt(process.env.GITDOCK_PORT, 10) || 3847;
3333
const HOST = "127.0.0.1"; // SECURITY: localhost only
3434

3535
// --- Configuration ---
36-
// When packaged with `pkg`, __dirname points to the snapshot (bundled assets).
36+
// When packaged (pkg or SEA), assets and config live next to the executable.
3737
// In dev mode, __dirname is the project directory.
38-
// APP_DIR = bundled assets (dashboard.html, logos) — always __dirname.
39-
// BASE_DIR = user data (config, repos) — workspace path (packaged) or __dirname (dev).
4038
const isPkg = typeof process.pkg !== "undefined";
41-
const APP_DIR = __dirname;
39+
const execBase = path.basename(process.execPath, ".exe").toLowerCase();
40+
const isStandalone = execBase === "gitdock";
41+
const APP_DIR = isPkg || isStandalone ? path.dirname(process.execPath) : __dirname;
4242
const workspaceModule = require("./workspace");
43-
let BASE_DIR = isPkg ? (workspaceModule.loadWorkspace() || path.dirname(process.execPath)) : __dirname;
43+
let BASE_DIR = (isPkg || isStandalone) ? (workspaceModule.loadWorkspace() || path.dirname(process.execPath)) : __dirname;
4444
let CONFIG_PATH = path.join(BASE_DIR, "config.json");
4545

4646
function reloadBaseDirFromWorkspace() {
47-
if (!isPkg) return;
47+
if (!isPkg && !isStandalone) return;
4848
const ws = workspaceModule.loadWorkspace();
4949
if (ws) {
5050
BASE_DIR = ws;

site/index.html

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@
221221
<a href="#how">How it works</a>
222222
<a href="#security">Security</a>
223223
<a href="https://github.com/gitdock-dev/gitdock" target="_blank" rel="noopener">GitHub</a>
224-
<a href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-nav">Download</a>
224+
<a id="nav-download" href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-nav">Download</a>
225225
</div>
226226
</div>
227227
</nav>
@@ -234,9 +234,10 @@
234234
<h1>Your Git projects.<br><span class="grad">One Space.</span></h1>
235235
<p>Stop wasting time switching between accounts, tabs, and terminals. Search, organize, and operate on all your repos from one local interface.</p>
236236
<div class="hero-actions">
237-
<a href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-hero btn-primary">
237+
<a id="hero-download" href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-hero btn-primary">
238238
Download
239239
</a>
240+
<a href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-hero btn-secondary" style="font-size:14px">All platforms</a>
240241
<a href="https://github.com/gitdock-dev/gitdock" target="_blank" rel="noopener" class="btn-hero btn-secondary">Star on GitHub</a>
241242
<a href="#features" class="btn-hero btn-secondary">Explore Features</a>
242243
</div>
@@ -505,7 +506,7 @@ <h4>Connect to the Hub</h4>
505506
<h2>Ready to take control?</h2>
506507
<p>Free and open source. Clone the repo or download a release. All your repos, organized in one dashboard.</p>
507508
<div style="margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
508-
<a href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-hero btn-primary" style="font-size:14px">
509+
<a id="cta-download" href="https://github.com/gitdock-dev/gitdock/releases" target="_blank" rel="noopener" class="btn-hero btn-primary" style="font-size:14px">
509510
Download Free
510511
</a>
511512
<a href="https://hub.gitdock.dev" target="_blank" rel="noopener" class="btn-hero btn-secondary" style="font-size:14px">
@@ -533,6 +534,30 @@ <h2>Ready to take control?</h2>
533534
</footer>
534535

535536
<script>
537+
// Set download links to latest release asset for current OS
538+
(function() {
539+
var ua = navigator.userAgent || navigator.platform || '';
540+
var platform = 'linux-x64';
541+
if (/Win(dows|32|64|CE)/i.test(ua) || navigator.platform === 'Win32') platform = 'windows-x64';
542+
else if (/Mac|Darwin|iPhone|iPad/i.test(ua) || navigator.platform === 'MacIntel') platform = 'macos-x64';
543+
var asset = 'gitdock-' + platform + '.zip';
544+
var releasesUrl = 'https://github.com/gitdock-dev/gitdock/releases';
545+
fetch('https://api.github.com/repos/gitdock-dev/gitdock/releases/latest', { headers: { Accept: 'application/vnd.github.v3+json' } })
546+
.then(function(r) { return r.ok ? r.json() : null; })
547+
.then(function(d) {
548+
if (!d || !d.tag_name) return;
549+
var hasAsset = d.assets && d.assets.some(function(a) { return a.name === asset; });
550+
if (hasAsset) {
551+
var url = 'https://github.com/gitdock-dev/gitdock/releases/download/' + d.tag_name + '/' + asset;
552+
['nav-download', 'hero-download', 'cta-download'].forEach(function(id) {
553+
var el = document.getElementById(id);
554+
if (el) el.href = url;
555+
});
556+
}
557+
})
558+
.catch(function() {});
559+
})();
560+
536561
// Stagger: assign incremental delay to grid children
537562
document.querySelectorAll('.pain-grid, .features-grid, .security-grid, .steps').forEach(function(grid) {
538563
grid.querySelectorAll('.fade-in').forEach(function(child, i) {

0 commit comments

Comments
 (0)