Skip to content

chore(release): v3.9.9 #155

chore(release): v3.9.9

chore(release): v3.9.9 #155

Workflow file for this run

name: Release
on:
push:
tags: ['v*']
jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Secret scan
run: npm run secret-scan
- name: Run tests
run: npm run test:release
docker:
name: Docker Build & Push
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build metadata
run: |
echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
echo "COMMIT_DATE=$(git log -1 --pretty=%cI)" >> "$GITHUB_ENV"
echo "BUILD_BRANCH=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
{
echo "COMMIT_MESSAGE<<EOF"
git log -1 --pretty=%s
echo "EOF"
} >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
# github.repository (= dwgx/WindsurfAPI) lowercases to "windsurfapi"
# without the hyphen the package.json npm name uses. Hardcode the
# kebab form so the image name stays "windsurf-api".
images: ghcr.io/${{ github.repository_owner }}/windsurf-api
tags: |
# Only tag as latest for stable releases (no pre-release suffix like -rc, -alpha, -beta)
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
# v2.0.6 -> 2.0.6
type=semver,pattern={{version}}
# v2.0.6 -> 2.0
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_VERSION=${{ env.VERSION }}
BUILD_COMMIT=${{ github.sha }}
BUILD_COMMIT_MESSAGE=${{ env.COMMIT_MESSAGE }}
BUILD_COMMIT_DATE=${{ env.COMMIT_DATE }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
- name: Summary
run: |
echo "## Docker Image Published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY"
echo '${{ steps.meta.outputs.tags }}' | tr ',' '\n' | sed 's/^/- `/' | sed 's/$/`/' >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Platform:** \`linux/amd64\`" >> "$GITHUB_STEP_SUMMARY"
macos-exe-arm64:
name: macOS single-binary (arm64)
needs: test
runs-on: macos-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build windsurfapi-macos-arm64
run: |
npm i -g @yao-pkg/pkg@6.21.0 esbuild
npm run build:bundle
mkdir -p dist-macos
pkg src/_bundle.cjs --config package.json --targets node22-macos-arm64 --output dist-macos/windsurfapi-macos-arm64
- name: Smoke-check the binary boots
run: node scripts/exe-boot-smoke.mjs dist-macos/windsurfapi-macos-arm64
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: windsurfapi-macos-arm64
path: dist-macos/windsurfapi-macos-arm64
if-no-files-found: error
# Use an explicit Intel label so the x64 build does not move to ARM.
# It is NOT in the release `needs`, so a failed or delayed x64 build never
# blocks the release. If it finishes in time its artifact is picked up;
# otherwise the release ships without the x64 binary.
macos-exe-x64:
name: macOS single-binary (x64 — non-blocking)
needs: test
runs-on: macos-26-intel
continue-on-error: true
timeout-minutes: 45
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build windsurfapi-macos-x64
run: |
npm i -g @yao-pkg/pkg@6.21.0 esbuild
npm run build:bundle
mkdir -p dist-macos
pkg src/_bundle.cjs --config package.json --targets node22-macos-x64 --output dist-macos/windsurfapi-macos-x64
- name: Smoke-check the binary boots
run: node scripts/exe-boot-smoke.mjs dist-macos/windsurfapi-macos-x64
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: windsurfapi-macos-x64
path: dist-macos/windsurfapi-macos-x64
if-no-files-found: error
windows-exe:
name: Windows single-exe
needs: test
runs-on: windows-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Match the pkg target (node22-win-x64). pkg-fetch 3.6.4 ships prebuilt
# base binaries for node22/24/26 but NOT node20 — a node20 target 404s
# then falls back to compiling Node from source (no VS toolchain on the
# runner → fails). node22 is LTS and has multiple prebuilts.
node-version: '22'
# Build the single .exe. pkg can't ingest our ESM tree directly:
# package.json has "type":"module", so pkg loads each src/*.js as ESM but
# its bytecode/fallback path emits CommonJS (module.exports) → "module is
# not defined in ES module scope" at boot (seen in rc1/rc2). Fix: esbuild
# first bundles the whole ESM graph into ONE CommonJS file (src/_bundle.cjs)
# — no per-file ESM/CJS ambiguity — then pkg wraps that single CJS file.
# The bundle keeps import.meta.url meaningful via a banner shim
# (pathToFileURL(__filename)), so config.js/server.js/devin-connect-models
# still resolve dashboard/ + data/ relative to the bundle; pkg's `assets`
# (in package.json) map those files into the snapshot at the same paths.
# The Windows runner has the network to fetch pkg's node22 base binary
# (node20 has none → don't use it). Zero runtime deps, so no `npm ci`.
- name: Build windsurfapi.exe
run: |
npm i -g @yao-pkg/pkg@6.21.0 esbuild
npm run build:bundle
pkg src/_bundle.cjs --config package.json --targets node22-win-x64 --output dist-windows/windsurfapi.exe
- name: Smoke-check the exe boots
shell: pwsh
run: |
$env:DEVIN_CONNECT = '1'; $env:HOST = '127.0.0.1'; $env:PORT = '3999'
$env:API_KEY = 'ci-smoke'; $env:DATA_DIR = "$env:RUNNER_TEMP\wa-smoke"
$env:WINDSURFAPI_NO_OPEN = '1' # don't try to launch a browser on the headless runner
$out = "$env:RUNNER_TEMP\exe-out.log"; $err = "$env:RUNNER_TEMP\exe-err.log"
$p = Start-Process -FilePath dist-windows\windsurfapi.exe -PassThru -WindowStyle Hidden -RedirectStandardOutput $out -RedirectStandardError $err
$ok = $false
for ($i = 0; $i -lt 40; $i++) {
if ($p.HasExited) { break }
try { $r = Invoke-WebRequest 'http://127.0.0.1:3999/health' -UseBasicParsing -TimeoutSec 3; if ($r.StatusCode -eq 200) { $ok = $true; break } } catch {}
Start-Sleep -Milliseconds 500
}
try { $d = Invoke-WebRequest 'http://127.0.0.1:3999/dashboard' -UseBasicParsing -TimeoutSec 5; $dashOk = ($d.StatusCode -eq 200 -and $d.Content.Length -gt 1000) } catch { $dashOk = $false }
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
Write-Host "--- exe stdout ---"; if (Test-Path $out) { Get-Content $out -Tail 40 }
Write-Host "--- exe stderr ---"; if (Test-Path $err) { Get-Content $err -Tail 40 }
if (-not $ok) { Write-Error 'exe did not answer /health'; exit 1 }
if (-not $dashOk) { Write-Error 'exe did not serve /dashboard (asset bundling broken?)'; exit 1 }
Write-Host 'exe smoke OK: /health 200 + /dashboard served from snapshot assets'
# Assemble the "解压即用" zip: the exe + the tray launcher scripts + icon,
# all in ONE flat folder. tray.ps1 auto-detects the sibling windsurfapi.exe
# and switches to EXE mode (no Node, no source tree). Double-click tray.vbs
# → system-tray icon; or double-click windsurfapi.exe → console window.
# Both are zero-dependency (the exe carries its own Node runtime).
- name: Package Windows zip (exe + tray)
shell: pwsh
run: |
$stage = "$env:RUNNER_TEMP\wa-zip"
New-Item -ItemType Directory -Path $stage -Force | Out-Null
Copy-Item dist-windows\windsurfapi.exe $stage\
Copy-Item deploy\windows\tray.vbs $stage\
Copy-Item deploy\windows\tray.ps1 $stage\
Copy-Item deploy\windows\tray.bat $stage\
Copy-Item deploy\windows\windsurfapi.ico $stage\ -ErrorAction SilentlyContinue
$readme = @(
'WindsurfAPI - Windows 免安装分发包',
'====================================',
'',
'两种启动方式(都无需安装 Node.js、无需源码):',
'',
'1) 系统托盘(推荐):双击 tray.vbs',
' -> 右下角系统匣出现图标,右键可【打开面板 / 复制面板密码 /',
' 复制 API Key / 状态 / 重启 / 退出】。无黑窗。',
'',
'2) 控制台:双击 windsurfapi.exe',
' -> 弹出一个控制台窗口显示日志。',
'',
'首次启动会自动生成 API_KEY 与 DASHBOARD_PASSWORD 并写入本目录的',
'.env,同时自动打开后台面板 http://127.0.0.1:3003/dashboard 。',
'在面板里贴上 Devin session token 即可上号,其余免配置。',
'',
'默认仅本机访问(HOST=127.0.0.1)。要对外提供服务,编辑 .env 把',
'HOST 改成 0.0.0.0 并保留一个强 API_KEY。'
) -join "`r`n"
Set-Content -Path "$stage\启动说明.txt" -Value $readme -Encoding UTF8
$zipPath = "$PWD\dist-windows\windsurfapi-windows.zip"
Get-ChildItem -Path $stage | Compress-Archive -DestinationPath $zipPath -Force
Write-Host "--- zip contents ---"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zr = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
$have = $zr.Entries.FullName
$have | ForEach-Object { Write-Host $_ }
$zr.Dispose()
$must = @('windsurfapi.exe','tray.vbs','tray.ps1','tray.bat')
foreach ($m in $must) { if ($have -notcontains $m) { Write-Error "zip missing $m"; exit 1 } }
Write-Host 'zip OK: exe + tray scripts present'
- name: Upload exe artifact
uses: actions/upload-artifact@v4
with:
name: windsurfapi-windows-exe
path: |
dist-windows/windsurfapi.exe
dist-windows/windsurfapi-windows.zip
if-no-files-found: error
release:
name: GitHub Release
needs: [docker, windows-exe, macos-exe-arm64]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Check for release notes
id: notes
run: |
NEW_PATH="docs/releases/RELEASE_NOTES_${{ steps.version.outputs.VERSION }}.md"
OLD_PATH="RELEASE_NOTES_${{ steps.version.outputs.VERSION }}.md"
if [[ -f "$NEW_PATH" ]]; then
echo "file=$NEW_PATH" >> "$GITHUB_OUTPUT"
echo "found=true" >> "$GITHUB_OUTPUT"
elif [[ -f "$OLD_PATH" ]]; then
echo "file=$OLD_PATH" >> "$GITHUB_OUTPUT"
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Download Windows exe
uses: actions/download-artifact@v4
with:
name: windsurfapi-windows-exe
path: dist-windows
- name: Download macOS arm64 binary
uses: actions/download-artifact@v4
with:
name: windsurfapi-macos-arm64
path: dist-macos
- name: Download macOS x64 binary (optional)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: windsurfapi-macos-x64
path: dist-macos
- name: Package macOS zip (arm64 + scripts; x64 included if available)
run: |
chmod +x dist-macos/windsurfapi-macos-arm64
[ -f dist-macos/windsurfapi-macos-x64 ] && chmod +x dist-macos/windsurfapi-macos-x64 || true
cp deploy/macos/run.sh dist-macos/
cp deploy/macos/run-background.sh dist-macos/
cp deploy/macos/stop.sh dist-macos/
cp deploy/macos/install-launchd.sh dist-macos/
cp deploy/macos/uninstall-launchd.sh dist-macos/
cp deploy/macos/README.md dist-macos/
chmod +x dist-macos/*.sh
cd dist-macos && zip -r ../windsurfapi-macos.zip . && cd ..
echo "macOS zip contents:"; unzip -l windsurfapi-macos.zip
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: v${{ steps.version.outputs.VERSION }}
body_path: ${{ steps.notes.outputs.found == 'true' && steps.notes.outputs.file || '' }}
generate_release_notes: ${{ steps.notes.outputs.found != 'true' }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
dist-windows/windsurfapi.exe
dist-windows/windsurfapi-windows.zip
dist-macos/windsurfapi-macos-arm64
windsurfapi-macos.zip