fix(scripts): use project-relative paths and portable toolchains

Remove fixed machine installation paths, prefer project-local Go and Node toolchains, and resolve build verification and browser test binary paths relative to the project. Add isolated discovery/relocation tests and document compatibility and validation boundaries.

Validation: 20 tool-discovery assertions, 3 Node path tests, startup isolation and script syntax checks passed. Frontend build passed; full Go compilation was blocked by unrelated concurrent Kubernetes changes.
This commit is contained in:
曾志威
2026-09-22 14:24:47 +08:00
parent 4d34f94e75
commit 8f27223fce
22 changed files with 355 additions and 34 deletions
+2
View File
@@ -2,6 +2,8 @@
data data
bin bin
artifacts artifacts
tools/go
tools/node
web/node_modules web/node_modules
web/test-results web/test-results
web/playwright-report web/playwright-report
+2
View File
@@ -21,6 +21,8 @@ Desktop.ini
# Local dependencies (versions are tracked in go.mod/go.sum and package-lock.json) # Local dependencies (versions are tracked in go.mod/go.sum and package-lock.json)
/vendor/ /vendor/
/tools/go/
/tools/node/
node_modules/ node_modules/
# Go workspace file # Go workspace file
+2
View File
@@ -65,6 +65,8 @@ Windows PowerShell:
.\scripts\build.ps1 .\scripts\build.ps1
``` ```
源码目录的 `start-dev.cmd` 需要 Go、Node.js 和 npm;它不是免编译的发行版启动器。脚本以项目自身位置为基准,优先使用 `tools/go/bin/go.exe` 和 `tools/node/node.exe`(完整工具链含 npm),否则从 PATH 查找,不写死机器安装路径。可将项目和工具目录一起复制到其他电脑;详见 [相对路径与便携构建](docs/build-output.md)。不需要开发环境时直接运行预编译发行版 `serviceport.exe`。
Linux / macOS: Linux / macOS:
```sh ```sh
+50 -2
View File
@@ -1,5 +1,53 @@
# 本地输出目录 # 本地输出目录
程序输出在 bin/;构建缓存使用 bin/build-cache,开发候选程序使用 bin/dev-build,Windows 单 EXE 发布目录使用 bin/releases。开发升级脚本的完整数据备份放在 bin/backups。 程序输出在 `bin/`;构建缓存使用 `bin/build-cache`,开发候选程序使用 `bin/dev-build`,Windows 单 EXE 发布目录使用 `bin/releases`。开发升级脚本的完整数据备份放在 `bin/backups`。
bin/backups 包含用户数据,不是可随意删除的编译缓存。旧 artifacts/ 中的测试产物和备份仍保留,不在合并时搬移或清理。验证可使用 artifacts/ 下的独立程序、临时端口和临时数据库,不覆盖运行中的 bin/serviceport.exe。 `bin/backups` 包含用户数据,不是可随意删除的编译缓存。旧 `artifacts/` 中的测试产物和备份仍保留,不在合并时搬移或清理。验证可使用 `artifacts/` 下的独立程序、临时端口和临时数据库,不覆盖运行中的 `bin/serviceport.exe`。
## 相对路径与跨机器使用
脚本不保存开发者盘符、用户目录或固定系统安装位置。项目根目录从脚本自身位置推导,与从哪个目录调用无关;复制整个项目到另一台电脑或带空格的其他目录后,不需要改脚本中的路径。
- CMD 入口通过 `%~dp0` 找脚本;PowerShell 通过 `$PSScriptRoot`;Shell 先切换到自身所在项目根目录;Node 通过 `import.meta.url` 定位项目。
- 默认输入、输出、缓存和测试二进制都相对项目根目录。`build.ps1 -OutputPath`、验证脚本的文件参数及 `SERVICEPORT_E2E_BINARY` 的相对值也按项目根目录解释,不按调用者的当前目录。
- 用户主动传入的外部文件路径仍兼容;脚本不会把它们作为跨机器默认值。已有 `SERVICEPORT_DATA` 自定义数据库路径和 AppData 兼容逻辑不改动。
- 运行时仍会把路径规范化用于启动进程、校验删除范围和防止递归备份。这些是动态计算的路径,不是写死在脚本中的绝对路径。Docker fixture 的容器内挂载点和入口路径属于容器契约,不是宿主机器路径,不随本次修改改变。
## Windows 构建工具查找
源码中的 `start-dev.cmd` 每次先准备当前源码对应的程序,因此仍需 Go、Node.js 和 npm;缓存不会免除开发工具依赖。只运行软件时,使用预编译发行版的 `serviceport.exe`,无需安装这些工具。
可以把对应操作系统与架构的完整工具链放进项目:
```text
serviceport/
tools/
go/bin/go.exe
node/node.exe
node/npm.cmd
node/node_modules/npm/...
scripts/
start-dev.cmd
```
上面只列关键文件;Go 和 Node 均需复制完整发行目录,不是只复制 EXE。`tools/go`、`tools/node` 不进入 Git 或 Docker 构建上下文。复制项目到另一台电脑时,如果需要便携构建,应自行一起复制这些工具目录;`git clone` 不会下载工具链。
Windows 查找顺序为:
1. 可选 `SERVICEPORT_GO` / `SERVICEPORT_NODE`,建议使用项目相对路径;显式路径无效时直接报错,不偷偷降级。
2. 项目内 `tools/go/bin/go.exe`、`tools/node/node.exe`,优先于机器 PATH。
3. 当前机器 PATH 中的对应应用程序,不再扫描 Program Files、AppData、用户目录或 SDK 安装位置。
Node 便携目录必须包含配套 npm。选定的 Node/npm 目录只在构建子进程的 PATH 中优先,不修改系统 PATH;直接使用 Node 执行 `build-web.mjs` 时也优先使用该 Node 目录中的 npm。Linux/macOS Shell 入口同样优先使用 `tools/go/bin`、`tools/node/bin`,没有则使用 PATH,工具需是对应系统版本。
默认工具目录齐全时直接双击 `start-dev.cmd`,无需设置路径。自定义相对位置示例:
```powershell
$env:SERVICEPORT_GO = 'tools/go/bin/go.exe'
$env:SERVICEPORT_NODE = 'tools/node/node.exe'
.\start-dev.cmd -BuildOnly
```
`-BuildOnly` 只构建候选程序,不启动、不停止或替换现有服务,也不迁移真实数据库。构建脚本固定 `GOTOOLCHAIN=go1.25.0`;本次不自动下载、安装或打包编译器,已有 Go 的工具链获取仍由 Go 自身及网络配置决定。工具缺失时在服务停机和处理数据之前失败,并提示放入项目工具目录或配置 PATH。安装工具或修改用户 PATH 后需重新打开终端。
验证记录见 [测试记录](testing.md)。
+10
View File
@@ -1,5 +1,15 @@
# 变更原因与设计记录 # 变更原因与设计记录
## 脚本相对路径与便携工具链(2026-09-22)
原因:用户在其他工作副本启动时遇到 `Go is required`,并要求所有脚本跨机器使用、删除固定绝对路径。原脚本包含固定 Go 安装位置,若干 Node 验证入口与浏览器服务还依赖调用者当前目录。
结果:移除 Go 固定安装目录与用户目录扫描,Go/Node 默认优先使用项目 tools 目录,再从 PATH 获取;可选显式覆盖按项目根目录解析相对值,错误覆盖不降级。构建、打包和矩阵脚本共享工具查找,Node 子进程使用配套运行环境。验证脚本与浏览器测试服务统一按脚本位置定位项目,文件参数和 SERVICEPORT_E2E_BINARY 的相对值不再依赖调用目录;Shell 入口增加项目便携工具链优先级,工具目录不进入 Git/Docker 上下文。
范围与兼容:涉及开发/打包/验证脚本和测试启动器,不改变业务 API、Go 1.25.0 约束、数据库选择或发行版运行。工具只安装在未加入 PATH 的系统目录时,需要自行加入 PATH 或放入项目 tools;验证脚本的相对参数现在统一以项目根目录为基准。用户显式外部路径仍兼容。运行时规范化、临时目录清理范围检查、停止服务前构建保护与容器内挂载契约保留;不自动安装软件或修改系统 PATH。
验证:新增 Go/Node 隔离查找和 Node 脚本搬移测试,覆盖项目路径优先级、相对覆盖、空格目录和异地调用。实际构建及协议/浏览器验证边界见 [测试记录](testing.md),操作见 [本地构建说明](build-output.md)。不以本机验证推断另一台机器已具备完整工具链。
## WebDAV 替代 S3 同步并提供独立备份(2026-09-22) ## WebDAV 替代 S3 同步并提供独立备份(2026-09-22)
原因:用户要求将 S3 同步改为 WebDAV 同步备份,不再保留双入口造成配置歧义,同时需要不影响共享同步状态的归档能力。 原因:用户要求将 S3 同步改为 WebDAV 同步备份,不再保留双入口造成配置歧义,同时需要不影响共享同步状态的归档能力。
+9
View File
@@ -1,5 +1,14 @@
# 测试与版本矩阵 # 测试与版本矩阵
## 2026-09-22 脚本相对路径与跨机器调用
- 从系统临时目录而非项目目录调用 Windows PowerShell 测试:go-toolchain.test.ps1 的 12 项断言、node-toolchain.test.ps1 的 8 项断言通过。覆盖项目便携工具优先于 PATH、项目相对覆盖、错误覆盖不降级、配套 npm、带空格目录与缺失提示。使用临时伪文件,不执行或安装工具,不修改持久环境变量。
- Node 内置测试 `node --test scripts/project-paths.test.mjs` 三项通过:验证脚本复制到带空格/方括号的新目录后,路径仍以新项目为基准,并从无关目录启动子进程验证;同时检查三个验证脚本与浏览器启动器使用统一解析,以及维护脚本没有固定盘符/机器安装目录扫描。
- 所有 scripts/*.ps1 的 PowerShell 语法解析通过;Git Bash 分别对 build.sh、test-matrix.sh 和容器 start-replica.sh 执行 `bash -n` 通过。未在实际 Linux/macOS 上构建,也未启动 Docker 协议矩阵。`git diff --check` 通过。
- 从项目外调用 scripts/start-dev.test.ps1 既有隔离回归通过,覆盖相对入口、调用目录、构建锁、参数/失败隔离、数据路径、备份及错误保护;使用临时程序 fixture,不接触真实服务和数据库。
- 实际从项目外调用 `start-dev.ps1 -BuildOnly`:正确定位项目、Go 与 Node,前端构建通过(仅现有大 chunk 警告);随后 Go 编译被工作区并行新增的 internal/outbound/kubernetes.go:184 中 done 重名/类型冲突阻断。本轮没有修改该业务文件,也没有用上次候选程序假装完成新构建。打包和 Playwright 后续步骤因新程序未生成而未执行。
- 上一版工具发现实现的 BuildOnly 成功日志保留在 artifacts/go-discovery,但不能替代最终相对路径版本的完整构建验证。本轮日志位于 artifacts/relative-scripts;未重跑全量 Go/go vet/UI,不宣称全量通过。未部署、提交或修改真实数据,其他并行工作区改动保持原样。
## 2026-09-22 WebDAV 同步备份替代 S3 ## 2026-09-22 WebDAV 同步备份替代 S3
- 使用 Go 1.25.0 运行 `go test ./... -count=1 -timeout=120s`:本次变更涉及的 backup、syncjob、store、api、model、MongoDB/HTTP 协议包均通过;全量仅 internal/cluster 的 TestMySQLReplicationAndGroupDiscovery/mysql-group-replication 失败,报 `cluster has no available primary or router`,与本文件此前记录的既有间歇失败一致。本轮未修改该逻辑,不将全量 Go 标记通过。日志 artifacts/webdav-sync/go-test.log。 - 使用 Go 1.25.0 运行 `go test ./... -count=1 -timeout=120s`:本次变更涉及的 backup、syncjob、store、api、model、MongoDB/HTTP 协议包均通过;全量仅 internal/cluster 的 TestMySQLReplicationAndGroupDiscovery/mysql-group-replication 失败,报 `cluster has no available primary or router`,与本文件此前记录的既有间歇失败一致。本轮未修改该逻辑,不将全量 Go 标记通过。日志 artifacts/webdav-sync/go-test.log。
+6 -3
View File
@@ -1,6 +1,6 @@
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path'; import { delimiter, dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
@@ -11,9 +11,12 @@ const cache = join(repo, 'bin', 'build-cache');
mkdirSync(cache, { recursive: true }); mkdirSync(cache, { recursive: true });
const hash = value => createHash('sha256').update(value).digest('hex'); const hash = value => createHash('sha256').update(value).digest('hex');
const readJSON = path => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } }; const readJSON = path => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
const runtimeEnvironment = { ...process.env };
const pathKey = Object.keys(runtimeEnvironment).find(key => key.toLowerCase() === 'path') || 'PATH';
runtimeEnvironment[pathKey] = dirname(process.execPath) + delimiter + (runtimeEnvironment[pathKey] || '');
const npm = args => process.platform === 'win32' const npm = args => process.platform === 'win32'
? execFileSync('cmd.exe', ['/d', '/s', '/c', `npm.cmd ${args.join(' ')}`], { cwd: web, stdio: 'inherit', windowsHide: true }) ? execFileSync('cmd.exe', ['/d', '/s', '/c', `npm.cmd ${args.join(' ')}`], { cwd: web, env: runtimeEnvironment, stdio: 'inherit', windowsHide: true })
: execFileSync('npm', args, { cwd: web, stdio: 'inherit' }); : execFileSync('npm', args, { cwd: web, env: runtimeEnvironment, stdio: 'inherit' });
const dependencyKey = hash(JSON.stringify([ const dependencyKey = hash(JSON.stringify([
process.version, process.arch, process.platform, process.version, process.arch, process.platform,
readFileSync(join(web, 'package.json'), 'utf8'), readFileSync(join(web, 'package.json'), 'utf8'),
+8 -6
View File
@@ -2,6 +2,7 @@ param([string]$Version, [switch]$SkipTests, [switch]$RebuildWeb, [string]$Output
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'build-lock.ps1') . (Join-Path $PSScriptRoot 'build-lock.ps1')
$buildLock = Enter-ServiceportBuildLock $buildLock = Enter-ServiceportBuildLock
$previousPath = $env:PATH
try { try {
$buildTimer = [Diagnostics.Stopwatch]::StartNew() $buildTimer = [Diagnostics.Stopwatch]::StartNew()
$phaseTimer = [Diagnostics.Stopwatch]::StartNew() $phaseTimer = [Diagnostics.Stopwatch]::StartNew()
@@ -11,20 +12,20 @@ function Write-BuildPhase([string]$Name) {
} }
. (Join-Path $PSScriptRoot 'version.ps1') . (Join-Path $PSScriptRoot 'version.ps1')
$buildInfo = Get-serviceportBuildInfo -Version $Version $buildInfo = Get-serviceportBuildInfo -Version $Version
Set-Location (Split-Path $PSScriptRoot -Parent) Set-Location -LiteralPath (Split-Path $PSScriptRoot -Parent)
$env:GOTOOLCHAIN = 'go1.25.0' $env:GOTOOLCHAIN = 'go1.25.0'
$env:GO111MODULE = 'on' $env:GO111MODULE = 'on'
$goCommand = Get-Command go -ErrorAction SilentlyContinue . (Join-Path $PSScriptRoot 'go-toolchain.ps1')
$goExecutable = if ($goCommand) { $goCommand.Source } else { 'C:\Program Files\Go\bin\go.exe' } $goExecutable = Get-ServiceportGoExecutable -ProjectRoot (Split-Path $PSScriptRoot -Parent)
if (!(Test-Path -LiteralPath $goExecutable)) { throw 'Go is required' } . (Join-Path $PSScriptRoot 'node-toolchain.ps1')
if (!(Get-Command node -ErrorAction SilentlyContinue) -or !(Get-Command npm.cmd -ErrorAction SilentlyContinue)) { throw 'Node.js and npm are required' } $nodeExecutable = Initialize-ServiceportNodeRuntime -ProjectRoot (Split-Path $PSScriptRoot -Parent)
$keepPath = Join-Path $PWD 'web/dist/.gitkeep' $keepPath = Join-Path $PWD 'web/dist/.gitkeep'
$keepBytes = [byte[]]@() $keepBytes = [byte[]]@()
if (Test-Path -LiteralPath $keepPath) { $keepBytes = [IO.File]::ReadAllBytes($keepPath) } if (Test-Path -LiteralPath $keepPath) { $keepBytes = [IO.File]::ReadAllBytes($keepPath) }
try { try {
$webArguments = @((Join-Path $PSScriptRoot 'build-web.mjs')) $webArguments = @((Join-Path $PSScriptRoot 'build-web.mjs'))
if ($RebuildWeb) { $webArguments += '--force' } if ($RebuildWeb) { $webArguments += '--force' }
node @webArguments & $nodeExecutable @webArguments
if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' } if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' }
} finally { } finally {
New-Item -ItemType Directory -Path (Split-Path $keepPath -Parent) -Force | Out-Null New-Item -ItemType Directory -Path (Split-Path $keepPath -Parent) -Force | Out-Null
@@ -47,5 +48,6 @@ Write-BuildPhase 'Go build'
Write-Host ('Build total: {0:N1}s' -f $buildTimer.Elapsed.TotalSeconds) Write-Host ('Build total: {0:N1}s' -f $buildTimer.Elapsed.TotalSeconds)
Write-Output $OutputPath Write-Output $OutputPath
} finally { } finally {
$env:PATH = $previousPath
$buildLock.Dispose() $buildLock.Dispose()
} }
+1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -eu set -eu
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
export PATH="$PWD/tools/go/bin:$PWD/tools/node/bin:$PATH"
export GOTOOLCHAIN=go1.25.0 export GOTOOLCHAIN=go1.25.0
export GO111MODULE=on export GO111MODULE=on
skip_tests=0 skip_tests=0
+26
View File
@@ -0,0 +1,26 @@
function Get-ServiceportGoExecutable {
param([Parameter(Mandatory = $true)][string]$ProjectRoot)
$ProjectRoot = [IO.Path]::GetFullPath($ProjectRoot)
if (![string]::IsNullOrWhiteSpace($env:SERVICEPORT_GO)) {
$explicit = [Environment]::ExpandEnvironmentVariables($env:SERVICEPORT_GO.Trim().Trim('"'))
if (![IO.Path]::IsPathRooted($explicit)) { $explicit = Join-Path $ProjectRoot $explicit }
$explicit = [IO.Path]::GetFullPath($explicit)
if (!(Test-Path -LiteralPath $explicit -PathType Leaf)) {
throw "SERVICEPORT_GO does not point to a file: $explicit. Use a project-relative go.exe path, or clear it to enable automatic discovery."
}
return $explicit
}
$portable = Join-Path $ProjectRoot 'tools/go/bin/go.exe'
if (Test-Path -LiteralPath $portable -PathType Leaf) { return $portable }
$command = Get-Command go.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command -and (Test-Path -LiteralPath $command.Source -PathType Leaf)) { return $command.Source }
throw (@(
'Go was not found. Running start-dev.cmd from a source checkout requires Go, Node.js and npm.'
'Install Go 1.25.0 on PATH and reopen the terminal, or set SERVICEPORT_GO to a project-relative go.exe path.'
'A portable Go installation may be placed at tools\go\bin\go.exe relative to the project.'
'To run without development tools, use the prebuilt serviceport.exe release instead of start-dev.cmd.'
('Checked tools/go/bin/go.exe under project: ' + $ProjectRoot + ', then go.exe on PATH.')
) -join [Environment]::NewLine)
}
+67
View File
@@ -0,0 +1,67 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'go-toolchain.ps1')
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('serviceport-go-test-' + [Guid]::NewGuid().ToString('N') + ' with spaces')
$project = Join-Path $fixture 'project'
$previous = @{}
$names = @('SERVICEPORT_GO')
foreach ($name in $names) { $previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
$script:commandSource = $null
$script:assertions = 0
function Get-Command {
[CmdletBinding()]
param([string]$Name, [string]$CommandType)
if ($Name -ne 'go.exe' -or $CommandType -ne 'Application') { throw 'Go lookup must exclude functions and aliases' }
if ($script:commandSource) { [PSCustomObject]@{ Source = $script:commandSource } }
}
function Assert-GoTest([bool]$Condition, [string]$Message) {
if (!$Condition) { throw $Message }
$script:assertions++
}
function New-GoFixture([string]$Path) {
New-Item -ItemType Directory -Path (Split-Path $Path -Parent) -Force | Out-Null
[IO.File]::WriteAllText($Path, 'resolution fixture, not an executable')
return [IO.Path]::GetFullPath($Path)
}
try {
New-Item -ItemType Directory -Path $project -Force | Out-Null
foreach ($name in $names) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') }
$pathGo = New-GoFixture (Join-Path $fixture 'on path/go.exe')
$script:commandSource = $pathGo
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $pathGo) 'PATH lookup failed'
$explicit = New-GoFixture (Join-Path $project 'custom Go/bin/go.exe')
$env:SERVICEPORT_GO = 'custom Go/bin/go.exe'
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $explicit) 'Project-relative override did not take priority over PATH'
$env:SERVICEPORT_GO = '"' + $explicit + '"'
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $explicit) 'Quoted absolute override failed'
$env:SERVICEPORT_GO = 'missing/go.exe'
$rejected = $false
try { Get-ServiceportGoExecutable $project | Out-Null } catch { $rejected = $_.Exception.Message -like 'SERVICEPORT_GO does not point to a file:*' }
Assert-GoTest $rejected 'Invalid explicit override silently fell back to PATH'
$env:SERVICEPORT_GO = $project
$rejected = $false
try { Get-ServiceportGoExecutable $project | Out-Null } catch { $rejected = $_.Exception.Message -like 'SERVICEPORT_GO does not point to a file:*' }
Assert-GoTest $rejected 'Directory accepted as an executable'
$env:SERVICEPORT_GO = $null
$script:commandSource = $null
$missingMessage = ''
try { Get-ServiceportGoExecutable $project | Out-Null } catch { $missingMessage = $_.Exception.Message }
Assert-GoTest ($missingMessage.Contains('Go 1.25.0') -and $missingMessage.Contains('SERVICEPORT_GO') -and $missingMessage.Contains('prebuilt serviceport.exe') -and $missingMessage.Contains($project)) 'Missing Go guidance is incomplete'
$portable = New-GoFixture (Join-Path $project 'tools/go/bin/go.exe')
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $portable) 'Project portable Go lookup failed'
$script:commandSource = $pathGo
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $portable) 'Portable Go did not take priority over machine PATH'
$env:SERVICEPORT_GO = 'custom Go/bin/go.exe'
Assert-GoTest ((Get-ServiceportGoExecutable $project) -eq $explicit) 'Explicit Go did not take priority over portable Go'
foreach ($entry in @('build.ps1', 'package.ps1', 'test-matrix.ps1')) {
$content = [IO.File]::ReadAllText((Join-Path $PSScriptRoot $entry))
Assert-GoTest ($content.Contains('Get-ServiceportGoExecutable -ProjectRoot') -and !$content.Contains("throw 'Go is required'")) ('Entry point does not use the common resolver: ' + $entry)
}
Write-Host "Go discovery tests passed: $script:assertions assertions. No toolchain was executed or installed."
} finally {
foreach ($name in $names) { [Environment]::SetEnvironmentVariable($name, $previous[$name], 'Process') }
Remove-Item -LiteralPath Function:\Get-Command
$resolved = [IO.Path]::GetFullPath($fixture)
$tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
if (!$resolved.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or !(Split-Path $resolved -Leaf).StartsWith('serviceport-go-test-')) { throw 'Unexpected fixture cleanup path' }
if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force }
}
+33
View File
@@ -0,0 +1,33 @@
function Initialize-ServiceportNodeRuntime {
param([Parameter(Mandatory = $true)][string]$ProjectRoot)
$ProjectRoot = [IO.Path]::GetFullPath($ProjectRoot)
$nodeExecutable = $null
$selected = $false
if (![string]::IsNullOrWhiteSpace($env:SERVICEPORT_NODE)) {
$nodeExecutable = [Environment]::ExpandEnvironmentVariables($env:SERVICEPORT_NODE.Trim().Trim('"'))
if (![IO.Path]::IsPathRooted($nodeExecutable)) { $nodeExecutable = Join-Path $ProjectRoot $nodeExecutable }
$selected = $true
} elseif (Test-Path -LiteralPath (Join-Path $ProjectRoot 'tools/node/node.exe') -PathType Leaf) {
$nodeExecutable = Join-Path $ProjectRoot 'tools/node/node.exe'
$selected = $true
} else {
$command = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) { $nodeExecutable = $command.Source }
}
if (!$nodeExecutable -or !(Test-Path -LiteralPath $nodeExecutable -PathType Leaf)) {
throw 'Node.js was not found. Put the complete Node.js distribution in tools/node, use SERVICEPORT_NODE with a project-relative node.exe path, or install Node.js and npm on PATH.'
}
$nodeExecutable = [IO.Path]::GetFullPath($nodeExecutable)
$nodeDirectory = Split-Path $nodeExecutable -Parent
$npmExecutable = Join-Path $nodeDirectory 'npm.cmd'
if (!(Test-Path -LiteralPath $npmExecutable -PathType Leaf)) {
$npmCommand = if (!$selected) { Get-Command npm.cmd -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 }
if (!$npmCommand -or !(Test-Path -LiteralPath $npmCommand.Source -PathType Leaf)) {
throw 'npm.cmd was not found beside Node.js. Copy the complete Node.js distribution, including npm.cmd and node_modules/npm, not only node.exe.'
}
$npmExecutable = $npmCommand.Source
}
$env:PATH = $nodeDirectory + [IO.Path]::PathSeparator + (Split-Path $npmExecutable -Parent) + [IO.Path]::PathSeparator + $env:PATH
return $nodeExecutable
}
+63
View File
@@ -0,0 +1,63 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'node-toolchain.ps1')
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('serviceport-node-test-' + [Guid]::NewGuid().ToString('N') + ' with spaces')
$project = Join-Path $fixture 'project'
$previousPath = $env:PATH
$previousNode = $env:SERVICEPORT_NODE
$script:commands = @{}
$script:assertions = 0
function Get-Command {
[CmdletBinding()]
param([string]$Name, [string]$CommandType)
if ($CommandType -ne 'Application') { throw 'Node lookup must exclude aliases and functions' }
if ($script:commands[$Name]) { [PSCustomObject]@{ Source = $script:commands[$Name] } }
}
function Assert-NodeTest([bool]$Condition, [string]$Message) {
if (!$Condition) { throw $Message }
$script:assertions++
}
function New-NodeFixture([string]$Directory, [bool]$WithNpm = $true) {
New-Item -ItemType Directory -Path $Directory -Force | Out-Null
$node = Join-Path $Directory 'node.exe'
[IO.File]::WriteAllText($node, 'resolution fixture, not executable')
if ($WithNpm) { [IO.File]::WriteAllText((Join-Path $Directory 'npm.cmd'), 'fixture') }
return $node
}
try {
$env:SERVICEPORT_NODE = $null
$pathNode = New-NodeFixture (Join-Path $fixture 'on path')
$script:commands['node.exe'] = $pathNode
Assert-NodeTest ((Initialize-ServiceportNodeRuntime $project) -eq $pathNode) 'PATH Node lookup failed'
$portable = New-NodeFixture (Join-Path $project 'tools/node')
Assert-NodeTest ((Initialize-ServiceportNodeRuntime $project) -eq $portable) 'Portable Node did not take priority over PATH'
Assert-NodeTest ($env:PATH.StartsWith((Split-Path $portable -Parent) + ';')) 'Child processes do not use selected Node'
$explicit = New-NodeFixture (Join-Path $project 'custom node')
$env:SERVICEPORT_NODE = 'custom node/node.exe'
Assert-NodeTest ((Initialize-ServiceportNodeRuntime $project) -eq $explicit) 'Project-relative Node override failed'
$env:SERVICEPORT_NODE = 'missing/node.exe'
$rejected = $false
try { Initialize-ServiceportNodeRuntime $project | Out-Null } catch { $rejected = $_.Exception.Message.Contains('Node.js was not found') }
Assert-NodeTest $rejected 'Invalid Node override silently fell back'
$incomplete = New-NodeFixture (Join-Path $project 'node only') $false
$script:commands['npm.cmd'] = Join-Path (Split-Path $pathNode -Parent) 'npm.cmd'
$env:SERVICEPORT_NODE = 'node only/node.exe'
$rejected = $false
try { Initialize-ServiceportNodeRuntime $project | Out-Null } catch { $rejected = $_.Exception.Message.Contains('complete Node.js distribution') }
Assert-NodeTest $rejected 'Incomplete portable Node was mixed with unrelated npm'
$env:SERVICEPORT_NODE = $null
$script:commands['node.exe'] = $incomplete
Assert-NodeTest ((Initialize-ServiceportNodeRuntime (Join-Path $fixture 'empty')) -eq $incomplete) 'Existing split PATH Node/npm stopped working'
$script:commands.Clear()
$rejected = $false
try { Initialize-ServiceportNodeRuntime (Join-Path $fixture 'empty') | Out-Null } catch { $rejected = $_.Exception.Message.Contains('tools/node') }
Assert-NodeTest $rejected 'Missing Node guidance is incomplete'
Write-Host "Node discovery tests passed: $script:assertions assertions. No runtime was executed or installed."
} finally {
$env:PATH = $previousPath
$env:SERVICEPORT_NODE = $previousNode
Remove-Item -LiteralPath Function:\Get-Command
$resolved = [IO.Path]::GetFullPath($fixture)
$tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
if (!$resolved.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or !(Split-Path $resolved -Leaf).StartsWith('serviceport-node-test-')) { throw 'Unexpected fixture cleanup path' }
if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force }
}
+9 -7
View File
@@ -2,6 +2,7 @@ param([string]$Version, [switch]$RebuildWeb)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'build-lock.ps1') . (Join-Path $PSScriptRoot 'build-lock.ps1')
$buildLock = Enter-ServiceportBuildLock $buildLock = Enter-ServiceportBuildLock
$previousPath = $env:PATH
try { try {
$totalTimer = [Diagnostics.Stopwatch]::StartNew() $totalTimer = [Diagnostics.Stopwatch]::StartNew()
$phaseTimer = [Diagnostics.Stopwatch]::StartNew() $phaseTimer = [Diagnostics.Stopwatch]::StartNew()
@@ -13,10 +14,10 @@ function Write-PhaseTime([string]$Name) {
$buildInfo = Get-serviceportBuildInfo -Version $Version $buildInfo = Get-serviceportBuildInfo -Version $Version
$Version = $buildInfo.Version $Version = $buildInfo.Version
$repo = Split-Path $PSScriptRoot -Parent $repo = Split-Path $PSScriptRoot -Parent
$goCommand = Get-Command go -ErrorAction SilentlyContinue . (Join-Path $PSScriptRoot 'go-toolchain.ps1')
$goExecutable = if ($goCommand) { $goCommand.Source } else { 'C:\Program Files\Go\bin\go.exe' } $goExecutable = Get-ServiceportGoExecutable -ProjectRoot $repo
if (!(Test-Path -LiteralPath $goExecutable)) { throw 'Go is required' } . (Join-Path $PSScriptRoot 'node-toolchain.ps1')
if (!(Get-Command npm.cmd -ErrorAction SilentlyContinue) -or !(Get-Command node -ErrorAction SilentlyContinue)) { throw 'Node.js and npm are required' } $nodeExecutable = Initialize-ServiceportNodeRuntime -ProjectRoot $repo
$stamp = (Get-Date -Format 'yyyyMMdd-HHmmss-fff') + '-' + [Guid]::NewGuid().ToString('N').Substring(0, 8) $stamp = (Get-Date -Format 'yyyyMMdd-HHmmss-fff') + '-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)
$releaseRoot = Join-Path $repo "bin/releases/$stamp" $releaseRoot = Join-Path $repo "bin/releases/$stamp"
$stage = [IO.Path]::GetFullPath((Join-Path $repo "bin/build-cache/package-$stamp")) $stage = [IO.Path]::GetFullPath((Join-Path $repo "bin/build-cache/package-$stamp"))
@@ -28,7 +29,7 @@ $keepPath = Join-Path $repo 'web/dist/.gitkeep'
$hadKeep = Test-Path -LiteralPath $keepPath $hadKeep = Test-Path -LiteralPath $keepPath
$keepBytes = [byte[]]@() $keepBytes = [byte[]]@()
if ($hadKeep) { $keepBytes = [IO.File]::ReadAllBytes($keepPath) } if ($hadKeep) { $keepBytes = [IO.File]::ReadAllBytes($keepPath) }
Push-Location $repo Push-Location -LiteralPath $repo
try { try {
$env:GOTOOLCHAIN = 'go1.25.0' $env:GOTOOLCHAIN = 'go1.25.0'
$env:GO111MODULE = 'on' $env:GO111MODULE = 'on'
@@ -38,14 +39,14 @@ try {
$env:CGO_ENABLED = '0' $env:CGO_ENABLED = '0'
$webArguments = @((Join-Path $PSScriptRoot 'build-web.mjs')) $webArguments = @((Join-Path $PSScriptRoot 'build-web.mjs'))
if ($RebuildWeb) { $webArguments += '--force' } if ($RebuildWeb) { $webArguments += '--force' }
node @webArguments & $nodeExecutable @webArguments
if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' } if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' }
Write-PhaseTime 'Frontend and dependencies' Write-PhaseTime 'Frontend and dependencies'
New-Item -ItemType Directory -Path $stage | Out-Null New-Item -ItemType Directory -Path $stage | Out-Null
& $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o $binary ./cmd/serviceport & $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o $binary ./cmd/serviceport
if ($LASTEXITCODE -ne 0) { throw 'serviceport release build failed' } if ($LASTEXITCODE -ne 0) { throw 'serviceport release build failed' }
Write-PhaseTime 'Go build' Write-PhaseTime 'Go build'
node (Join-Path $PSScriptRoot 'verify-package.mjs') $stage & $nodeExecutable (Join-Path $PSScriptRoot 'verify-package.mjs') $stage
if ($LASTEXITCODE -ne 0) { throw 'Package verification failed' } if ($LASTEXITCODE -ne 0) { throw 'Package verification failed' }
Write-PhaseTime 'Executable verification' Write-PhaseTime 'Executable verification'
# Publish only after verification. The generated paths must stay in this repository. # Publish only after verification. The generated paths must stay in this repository.
@@ -73,5 +74,6 @@ try {
Pop-Location Pop-Location
} }
} finally { } finally {
$env:PATH = $previousPath
$buildLock.Dispose() $buildLock.Dispose()
} }
+5
View File
@@ -0,0 +1,5 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const resolveProjectPath = path => resolve(projectRoot, path);
+47
View File
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { test } from 'node:test';
import { pathToFileURL } from 'node:url';
import { projectRoot, resolveProjectPath } from './project-paths.mjs';
test('relative paths use the relocated script root instead of the caller directory', async () => {
const fixture = mkdtempSync(join(tmpdir(), 'serviceport-path-test-'));
try {
const relocated = join(fixture, 'moved project [spaces]');
mkdirSync(join(relocated, 'scripts'), { recursive: true });
const module = join(relocated, 'scripts', 'project-paths.mjs');
copyFileSync(resolveProjectPath('scripts/project-paths.mjs'), module);
const { resolveProjectPath: resolveMoved } = await import(pathToFileURL(module));
assert.equal(resolveMoved('bin/serviceport.exe'), join(relocated, 'bin', 'serviceport.exe'));
assert.equal(resolveMoved(join(fixture, 'external.exe')), join(fixture, 'external.exe'));
const output = execFileSync(process.execPath, ['--input-type=module', '-e',
`import { resolveProjectPath } from ${JSON.stringify(pathToFileURL(module).href)}; console.log(resolveProjectPath('bin/serviceport.exe'));`],
{ cwd: tmpdir(), encoding: 'utf8', windowsHide: true });
assert.equal(output.trim(), join(relocated, 'bin', 'serviceport.exe'));
} finally {
assert.equal(dirname(resolve(fixture)), resolve(tmpdir()));
assert(basename(fixture).startsWith('serviceport-path-test-'));
rmSync(fixture, { recursive: true, force: true });
}
});
test('verification and browser launcher resolve binary arguments from the project', () => {
assert.equal(resolveProjectPath(''), projectRoot);
for (const name of ['verify-package.mjs', 'verify-desktop.mjs', 'verify-random-port.mjs']) {
assert.match(readFileSync(resolveProjectPath(`scripts/${name}`), 'utf8'), /resolveProjectPath\(process\.argv\[2\]\)/);
}
assert.match(readFileSync(resolveProjectPath('web/tests/start-server.mjs'), 'utf8'), /resolveProjectPath\(process\.env\.SERVICEPORT_E2E_BINARY/);
});
test('maintained scripts contain no fixed Windows installation or developer paths', () => {
const scripts = readdirSync(resolveProjectPath('scripts')).filter(name => /\.(ps1|mjs|sh)$/.test(name) && !name.includes('.test.'));
const paths = [...scripts.map(name => `scripts/${name}`), 'start-dev.cmd', 'package.cmd', 'web/tests/start-server.mjs'];
for (const path of paths) {
const source = readFileSync(resolveProjectPath(path), 'utf8');
assert.doesNotMatch(source, /\b[A-Za-z]:[\\/]/, `Fixed drive path in ${path}`);
assert.doesNotMatch(source, /\$env:(?:ProgramFiles|ProgramW6432|LOCALAPPDATA|USERPROFILE)/i, `Machine installation scan in ${path}`);
}
});
+3 -4
View File
@@ -5,15 +5,14 @@ $compose = Join-Path $repo 'deploy/compose.test.yml'
$project = 'serviceport-matrix-' + [Guid]::NewGuid().ToString('N').Substring(0, 10) $project = 'serviceport-matrix-' + [Guid]::NewGuid().ToString('N').Substring(0, 10)
$tempDir = Join-Path ([IO.Path]::GetTempPath()) $project $tempDir = Join-Path ([IO.Path]::GetTempPath()) $project
$caFile = Join-Path $tempDir 'ca.pem' $caFile = Join-Path $tempDir 'ca.pem'
$goCommand = Get-Command go -ErrorAction SilentlyContinue . (Join-Path $PSScriptRoot 'go-toolchain.ps1')
$goExecutable = if ($goCommand) { $goCommand.Source } else { 'C:\Program Files\Go\bin\go.exe' } $goExecutable = Get-ServiceportGoExecutable -ProjectRoot $repo
if (!(Test-Path -LiteralPath $goExecutable)) { throw 'Go is required' }
Get-Command docker -ErrorAction Stop | Out-Null Get-Command docker -ErrorAction Stop | Out-Null
$names = @('GOTOOLCHAIN', 'SERVICEPORT_MYSQL_TEST_ADDR', 'SERVICEPORT_MYSQL_TEST_USER', 'SERVICEPORT_MYSQL_TEST_PASSWORD', 'SERVICEPORT_MYSQL_TEST_CA', 'SERVICEPORT_MONGO_TEST_ADDR', 'SERVICEPORT_MONGO_TEST_USER', 'SERVICEPORT_MONGO_TEST_PASSWORD', 'SERVICEPORT_MONGO_TEST_CA', 'SERVICEPORT_SSH_TEST_ADDR', 'SERVICEPORT_SSH_TEST_USER', 'SERVICEPORT_SSH_TEST_PASSWORD', 'SERVICEPORT_TEST_SOCKS_ADDR', 'SERVICEPORT_TEST_SOCKS_USER', 'SERVICEPORT_TEST_SOCKS_PASSWORD') $names = @('GOTOOLCHAIN', 'SERVICEPORT_MYSQL_TEST_ADDR', 'SERVICEPORT_MYSQL_TEST_USER', 'SERVICEPORT_MYSQL_TEST_PASSWORD', 'SERVICEPORT_MYSQL_TEST_CA', 'SERVICEPORT_MONGO_TEST_ADDR', 'SERVICEPORT_MONGO_TEST_USER', 'SERVICEPORT_MONGO_TEST_PASSWORD', 'SERVICEPORT_MONGO_TEST_CA', 'SERVICEPORT_SSH_TEST_ADDR', 'SERVICEPORT_SSH_TEST_USER', 'SERVICEPORT_SSH_TEST_PASSWORD', 'SERVICEPORT_TEST_SOCKS_ADDR', 'SERVICEPORT_TEST_SOCKS_USER', 'SERVICEPORT_TEST_SOCKS_PASSWORD')
$previous = @{} $previous = @{}
foreach ($name in $names) { $previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } foreach ($name in $names) { $previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
New-Item -ItemType Directory -Path $tempDir | Out-Null New-Item -ItemType Directory -Path $tempDir | Out-Null
Push-Location $repo Push-Location -LiteralPath $repo
try { try {
$env:GOTOOLCHAIN = 'go1.25.0' $env:GOTOOLCHAIN = 'go1.25.0'
docker compose -f $compose -p $project up -d --build --wait --wait-timeout 300 docker compose -f $compose -p $project up -d --build --wait --wait-timeout 300
+1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
export PATH="$PWD/tools/go/bin:$PWD/tools/node/bin:$PATH"
command -v docker >/dev/null command -v docker >/dev/null
command -v go >/dev/null command -v go >/dev/null
export GOTOOLCHAIN=go1.25.0 export GOTOOLCHAIN=go1.25.0
+3 -2
View File
@@ -5,13 +5,14 @@ import { randomBytes, X509Certificate } from 'node:crypto'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs' import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { resolve, join } from 'node:path' import { join } from 'node:path'
import { resolveProjectPath } from './project-paths.mjs'
import { setTimeout as delay } from 'node:timers/promises' import { setTimeout as delay } from 'node:timers/promises'
import { DatabaseSync } from 'node:sqlite' import { DatabaseSync } from 'node:sqlite'
const require = createRequire(new URL('../web/package.json', import.meta.url)) const require = createRequire(new URL('../web/package.json', import.meta.url))
const { chromium } = require('@playwright/test') const { chromium } = require('@playwright/test')
assert(process.argv[2], 'Provide the native serviceport executable') assert(process.argv[2], 'Provide the native serviceport executable')
const binary = resolve(process.argv[2]) const binary = resolveProjectPath(process.argv[2])
const temp = mkdtempSync(join(tmpdir(), 'serviceport-desktop-')) const temp = mkdtempSync(join(tmpdir(), 'serviceport-desktop-'))
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.toUpperCase().startsWith('SERVICEPORT_') && k.toUpperCase() !== 'APPDATA')) const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.toUpperCase().startsWith('SERVICEPORT_') && k.toUpperCase() !== 'APPDATA'))
Object.assign(env, { APPDATA: join(temp, 'unused-appdata'), SERVICEPORT_DATA: join(temp, 'data', 'serviceport.sqlite'), SERVICEPORT_NO_BROWSER: '1' }) Object.assign(env, { APPDATA: join(temp, 'unused-appdata'), SERVICEPORT_DATA: join(temp, 'data', 'serviceport.sqlite'), SERVICEPORT_NO_BROWSER: '1' })
+2 -1
View File
@@ -6,9 +6,10 @@ import { createServer } from 'node:net'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { resolve, join, dirname, basename, sep } from 'node:path' import { resolve, join, dirname, basename, sep } from 'node:path'
import { setTimeout as delay } from 'node:timers/promises' import { setTimeout as delay } from 'node:timers/promises'
import { resolveProjectPath } from './project-paths.mjs'
assert(process.argv[2], 'Provide the single-binary release directory or executable') assert(process.argv[2], 'Provide the single-binary release directory or executable')
const input = resolve(process.argv[2]) const input = resolveProjectPath(process.argv[2])
const directory = statSync(input).isDirectory() ? input : dirname(input) const directory = statSync(input).isDirectory() ? input : dirname(input)
const filename = process.platform === 'win32' ? 'serviceport.exe' : 'serviceport' const filename = process.platform === 'win32' ? 'serviceport.exe' : 'serviceport'
const binary = join(directory, filename) const binary = join(directory, filename)
+2 -1
View File
@@ -6,9 +6,10 @@ import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { basename, join, resolve, sep } from 'node:path'; import { basename, join, resolve, sep } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises'; import { setTimeout as delay } from 'node:timers/promises';
import { resolveProjectPath } from './project-paths.mjs';
assert(process.argv[2], 'Provide the freshly built serviceport executable'); assert(process.argv[2], 'Provide the freshly built serviceport executable');
const binary = resolve(process.argv[2]); const binary = resolveProjectPath(process.argv[2]);
const temporaryRoot = resolve(tmpdir()); const temporaryRoot = resolve(tmpdir());
const directory = mkdtempSync(join(temporaryRoot, 'serviceport-random-port-')); const directory = mkdtempSync(join(temporaryRoot, 'serviceport-random-port-'));
const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('SERVICEPORT_'))); const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('SERVICEPORT_')));
+4 -8
View File
@@ -1,16 +1,12 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { resolve, join } from "node:path"; import { join } from "node:path";
import { resolveProjectPath } from '../../scripts/project-paths.mjs';
const directory = mkdtempSync(join(tmpdir(), "serviceport-e2e-")); const directory = mkdtempSync(join(tmpdir(), "serviceport-e2e-"));
const authenticated = process.argv.includes('--auth'); const authenticated = process.argv.includes('--auth');
const binary = const binary = resolveProjectPath(process.env.SERVICEPORT_E2E_BINARY ||
process.env.SERVICEPORT_E2E_BINARY || join('bin', process.platform === 'win32' ? 'serviceport.exe' : 'serviceport'));
resolve(
"..",
"bin",
process.platform === "win32" ? "serviceport.exe" : "serviceport",
);
const child = spawn(binary, [], { const child = spawn(binary, [], {
windowsHide: true, windowsHide: true,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],