chore(release): v3.9.9 #155
Workflow file for this run
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
| 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 |