mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-30 23:01:23 +08:00
🐛 fix(driver/sidebar): 修复驱动代理 revision 校验与外部SQL目录重复显示
- driver:下载或总包中的 driver-agent revision 过旧时跳过该候选并继续 fallback - driver:新增发布资产 revision 校验脚本并接入 dev/release CI - sidebar:修复 v2 表/视图等筛选下重复显示外部 SQL 目录 - test:补充 driver-agent fallback 与侧栏筛选回归测试
This commit is contained in:
5
.github/workflows/dev-build.yml
vendored
5
.github/workflows/dev-build.yml
vendored
@@ -436,6 +436,11 @@ jobs:
|
||||
bash ./tools/compress-driver-artifact.sh "${OUTPUT_PATH}" "$TARGET_PLATFORM" "${{ matrix.os_name }}/${OUTPUT}"
|
||||
done
|
||||
|
||||
bash ./tools/verify-driver-agent-revisions.sh \
|
||||
--assets-dir drivers \
|
||||
--platform "$TARGET_PLATFORM" \
|
||||
--drivers "$CHANGED_DRIVER_AGENTS"
|
||||
|
||||
# macOS Packaging
|
||||
- name: Package macOS DMG
|
||||
if: contains(matrix.platform, 'darwin')
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -428,6 +428,11 @@ jobs:
|
||||
bash ./tools/compress-driver-artifact.sh "${OUTPUT_PATH}" "$TARGET_PLATFORM" "${{ matrix.os_name }}/${OUTPUT}"
|
||||
done
|
||||
|
||||
bash ./tools/verify-driver-agent-revisions.sh \
|
||||
--assets-dir drivers \
|
||||
--platform "$TARGET_PLATFORM" \
|
||||
--drivers "$CHANGED_DRIVER_AGENTS"
|
||||
|
||||
# macOS Packaging
|
||||
- name: Package macOS DMG
|
||||
if: contains(matrix.platform, 'darwin')
|
||||
|
||||
@@ -1096,6 +1096,43 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node) => node.key)).toEqual(['conn-main-events']);
|
||||
});
|
||||
|
||||
it('hides external SQL roots from v2 object kind filters', () => {
|
||||
const tree = [
|
||||
{
|
||||
title: 'front_end_sys',
|
||||
key: 'conn-main',
|
||||
type: 'database' as const,
|
||||
children: [
|
||||
{
|
||||
title: '表',
|
||||
key: 'conn-main-tables',
|
||||
type: 'object-group' as const,
|
||||
dataRef: { groupKey: 'tables' },
|
||||
children: [{ title: 'users', key: 'users', type: 'table' as const }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '外部 SQL 目录',
|
||||
key: 'external-sql-root',
|
||||
type: 'external-sql-root' as const,
|
||||
children: [
|
||||
{
|
||||
title: 'scripts',
|
||||
key: 'external-sql-folder:scripts',
|
||||
type: 'external-sql-folder' as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'all').map((node) => node.key)).toEqual([
|
||||
'conn-main',
|
||||
'external-sql-root',
|
||||
]);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables').map((node) => node.key)).toEqual(['conn-main']);
|
||||
});
|
||||
|
||||
it('adds rename to the saved query context menu', () => {
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
|
||||
@@ -491,7 +491,7 @@ export const filterV2ExplorerTreeByKind = (
|
||||
|
||||
const visit = (node: TreeNode): TreeNode | null => {
|
||||
if (node.type === 'external-sql-root') {
|
||||
return node;
|
||||
return null;
|
||||
}
|
||||
const groupKey = String(node?.dataRef?.groupKey || '');
|
||||
if (node.type === 'object-group') {
|
||||
|
||||
@@ -3374,6 +3374,19 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
if a != nil {
|
||||
a.emitDriverDownloadProgress(driverType, "downloading", 10, 100, planMessage)
|
||||
}
|
||||
validateInstalledCandidateRevision := func(source string) error {
|
||||
if _, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion); revisionErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
for _, supportName := range optionalDriverSupportFileNames(driverType) {
|
||||
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
|
||||
}
|
||||
if strings.TrimSpace(source) != "" {
|
||||
return fmt.Errorf("%s: %w", source, revisionErr)
|
||||
}
|
||||
return revisionErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !skipReuseCandidate {
|
||||
if sourcePath, ok := findExistingOptionalDriverAgentCandidate(definition, executablePath); ok {
|
||||
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
|
||||
@@ -3387,6 +3400,9 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
if hashErr != nil {
|
||||
return "", "", fmt.Errorf("计算预置 %s 驱动代理摘要失败:%w", displayName, hashErr)
|
||||
}
|
||||
if revisionErr := validateInstalledCandidateRevision(sourcePath); revisionErr != nil {
|
||||
return "", "", fmt.Errorf("预置 %s 驱动代理 revision 校验失败:%w", displayName, revisionErr)
|
||||
}
|
||||
return "file://" + sourcePath, hash, nil
|
||||
}
|
||||
}
|
||||
@@ -3421,6 +3437,11 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
}
|
||||
hash, dlErr := downloadOptionalDriverAgentBinary(a, definition, candidateURL, executablePath)
|
||||
if dlErr == nil {
|
||||
if revisionErr := validateInstalledCandidateRevision(candidateURL); revisionErr != nil {
|
||||
logger.Warnf("预编译 %s 驱动代理 revision 校验失败,url=%s err=%v", displayName, candidateURL, revisionErr)
|
||||
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", candidateURL, strings.TrimSpace(revisionErr.Error())))
|
||||
continue
|
||||
}
|
||||
return candidateURL, hash, nil
|
||||
}
|
||||
logger.Warnf("下载预编译 %s 驱动代理失败,url=%s err=%v", displayName, candidateURL, dlErr)
|
||||
@@ -3439,6 +3460,11 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
}
|
||||
source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath)
|
||||
if bundleErr == nil {
|
||||
if revisionErr := validateInstalledCandidateRevision(source); revisionErr != nil {
|
||||
logger.Warnf("驱动总包 %s 代理 revision 校验失败,source=%s err=%v", displayName, source, revisionErr)
|
||||
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", source, strings.TrimSpace(revisionErr.Error())))
|
||||
continue
|
||||
}
|
||||
return source, hash, nil
|
||||
}
|
||||
logger.Warnf("从驱动总包提取 %s 驱动代理失败,url=%s err=%v", displayName, bundleURL, bundleErr)
|
||||
@@ -3847,7 +3873,7 @@ func shouldPreferSourceBuildBeforeDownloadForBuildType(buildType string, driverT
|
||||
|
||||
func shouldRequireSourceBuildBeforeDownloadForBuildType(buildType string, driverType string, selectedVersion string) bool {
|
||||
_ = selectedVersion
|
||||
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
|
||||
if normalizeDriverType(driverType) == "duckdb" {
|
||||
return false
|
||||
}
|
||||
return shouldPreferDevelopmentDriverAgentSourceBuild(buildType, driverType)
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/db"
|
||||
)
|
||||
|
||||
func TestResolveVersionedDriverOptionUsesPublishedMongoV1Release(t *testing.T) {
|
||||
@@ -727,6 +729,95 @@ func TestDownloadOptionalDriverAgentFromBundleSharesConcurrentDownload(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureOptionalDriverAgentBinaryFallsBackAfterStaleDownloadRevision(t *testing.T) {
|
||||
originalProbe := optionalDriverAgentMetadataProbe
|
||||
originalGoBinaryLookPath := goBinaryLookPath
|
||||
t.Cleanup(func() {
|
||||
optionalDriverAgentMetadataProbe = originalProbe
|
||||
goBinaryLookPath = originalGoBinaryLookPath
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
staleAgent := filepath.Join(tmpDir, "stale-driver-agent")
|
||||
if runtime.GOOS == "windows" {
|
||||
staleAgent += ".exe"
|
||||
}
|
||||
writeSelfExecutable(t, staleAgent)
|
||||
|
||||
staleServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, staleAgent)
|
||||
}))
|
||||
defer staleServer.Close()
|
||||
|
||||
projectRoot := filepath.Join(tmpDir, "project")
|
||||
if err := os.MkdirAll(filepath.Join(projectRoot, "cmd", "optional-driver-agent"), 0o755); err != nil {
|
||||
t.Fatalf("create project root failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(projectRoot, "go.mod"), []byte("module GoNavi-Wails\n"), 0o644); err != nil {
|
||||
t.Fatalf("write go.mod failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(projectRoot, "cmd", "optional-driver-agent", "main.go"), []byte("package main\n"), 0o644); err != nil {
|
||||
t.Fatalf("write optional agent main failed: %v", err)
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd failed: %v", err)
|
||||
}
|
||||
if err := os.Chdir(projectRoot); err != nil {
|
||||
t.Fatalf("chdir project root failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Chdir(wd); err != nil {
|
||||
t.Fatalf("restore cwd failed: %v", err)
|
||||
}
|
||||
})
|
||||
goScript := filepath.Join(tmpDir, "fake-go")
|
||||
if runtime.GOOS == "windows" {
|
||||
goScript += ".bat"
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.WriteFile(goScript, []byte("@echo off\r\nset out=\r\n:loop\r\nif \"%1\"==\"\" goto done\r\nif \"%1\"==\"-o\" (set out=%2& shift& shift& goto loop)\r\nshift\r\ngoto loop\r\n:done\r\ncopy /Y \"%GONAVI_TEST_BUILT_AGENT%\" \"%out%\" >nul\r\n"), 0o755); err != nil {
|
||||
t.Fatalf("write fake go script failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := os.WriteFile(goScript, []byte("#!/usr/bin/env sh\nout=\"\"\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"-o\" ]; then out=\"$2\"; shift 2; continue; fi\n shift\ndone\ncp \"$GONAVI_TEST_BUILT_AGENT\" \"$out\"\n"), 0o755); err != nil {
|
||||
t.Fatalf("write fake go script failed: %v", err)
|
||||
}
|
||||
}
|
||||
goBinaryLookPath = func(file string) (string, error) {
|
||||
return goScript, nil
|
||||
}
|
||||
t.Setenv("GONAVI_TEST_BUILT_AGENT", staleAgent)
|
||||
|
||||
probeCount := 0
|
||||
optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) {
|
||||
probeCount++
|
||||
revision := "src-stale-agent"
|
||||
if probeCount > 1 {
|
||||
revision = db.OptionalDriverAgentRevision(driverType)
|
||||
}
|
||||
return db.OptionalDriverAgentMetadata{
|
||||
DriverType: driverType,
|
||||
AgentRevision: revision,
|
||||
}, nil
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(tmpDir, optionalDriverExecutableBaseName("sqlserver"))
|
||||
source, _, err := ensureOptionalDriverAgentBinary(
|
||||
nil,
|
||||
driverDefinition{Type: "sqlserver", Name: "SQL Server"},
|
||||
targetPath,
|
||||
staleServer.URL,
|
||||
"1.9.6",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected stale direct download to fall back to source build, got %v", err)
|
||||
}
|
||||
if source != "local://go-build/sqlserver-driver-agent" {
|
||||
t.Fatalf("expected source build fallback, got %q", source)
|
||||
}
|
||||
}
|
||||
|
||||
func seedReleaseAssetSizeCache(t *testing.T, cacheKey string, sizeByKey map[string]int64) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
164
tools/verify-driver-agent-revisions.sh
Executable file
164
tools/verify-driver-agent-revisions.sh
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./tools/verify-driver-agent-revisions.sh --assets-dir <目录> --platform <GOOS/GOARCH> --drivers <列表>
|
||||
|
||||
说明:
|
||||
校验已构建 driver-agent 资产返回的 agentRevision 是否等于当前源码生成的 revision。
|
||||
EOF
|
||||
}
|
||||
|
||||
assets_dir=""
|
||||
target_platform=""
|
||||
driver_csv=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--assets-dir)
|
||||
assets_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
target_platform="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--drivers)
|
||||
driver_csv="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数:$1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$assets_dir" || -z "$target_platform" || -z "$driver_csv" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$target_platform" != */* ]]; then
|
||||
echo "--platform 参数格式错误,应为 GOOS/GOARCH,例如 darwin/arm64" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
goos="${target_platform%%/*}"
|
||||
goarch="${target_platform##*/}"
|
||||
platform_dir="Unknown"
|
||||
case "$goos" in
|
||||
windows) platform_dir="Windows" ;;
|
||||
darwin) platform_dir="MacOS" ;;
|
||||
linux) platform_dir="Linux" ;;
|
||||
esac
|
||||
|
||||
normalize_driver() {
|
||||
local value
|
||||
value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
||||
case "$value" in
|
||||
doris|diros) echo "diros" ;;
|
||||
opengauss|open_gauss|open-gauss) echo "opengauss" ;;
|
||||
elasticsearch|elastic) echo "elasticsearch" ;;
|
||||
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|iris|mongodb|tdengine|clickhouse)
|
||||
echo "$value"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
public_driver_name() {
|
||||
case "$1" in
|
||||
diros) echo "doris" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
expected_revision_for() {
|
||||
local target="$1"
|
||||
awk -v target="$target" '
|
||||
$0 ~ "\"" target "\"" {
|
||||
if (match($0, /"src-[^"]+"/)) {
|
||||
value=substr($0, RSTART + 1, RLENGTH - 2)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
}
|
||||
' internal/db/driver_agent_revisions_gen.go
|
||||
}
|
||||
|
||||
agent_path_for() {
|
||||
local driver="$1"
|
||||
local public_name asset
|
||||
public_name="$(public_driver_name "$driver")"
|
||||
asset="${public_name}-driver-agent-${goos}-${goarch}"
|
||||
if [[ "$goos" == "windows" ]]; then
|
||||
asset="${asset}.exe"
|
||||
fi
|
||||
printf '%s\n' "${assets_dir%/}/${platform_dir}/${asset}"
|
||||
}
|
||||
|
||||
probe_agent_revision() {
|
||||
local agent_path="$1"
|
||||
local request
|
||||
request='{"id":1,"method":"metadata"}'
|
||||
printf '%s\n' "$request" | "$agent_path" | python3 -c '
|
||||
import json
|
||||
import sys
|
||||
|
||||
line = sys.stdin.readline()
|
||||
payload = json.loads(line)
|
||||
data = payload.get("data") or {}
|
||||
print(data.get("agentRevision", ""))
|
||||
'
|
||||
}
|
||||
|
||||
declare -a raw_drivers=()
|
||||
IFS=',' read -r -a raw_drivers <<<"$driver_csv"
|
||||
|
||||
failed=0
|
||||
for raw_driver in "${raw_drivers[@]}"; do
|
||||
[[ -n "$raw_driver" ]] || continue
|
||||
driver="$(normalize_driver "$raw_driver")"
|
||||
if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" != "amd64" ]]; then
|
||||
echo "⚠️ 跳过 duckdb revision 校验($target_platform 不构建 agent)"
|
||||
continue
|
||||
fi
|
||||
|
||||
expected="$(expected_revision_for "$driver")"
|
||||
if [[ -z "$expected" ]]; then
|
||||
echo "❌ $driver 缺少期望 revision"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
agent_path="$(agent_path_for "$driver")"
|
||||
if [[ ! -f "$agent_path" ]]; then
|
||||
echo "❌ $driver 缺少 driver-agent 资产:$agent_path"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
chmod +x "$agent_path" 2>/dev/null || true
|
||||
|
||||
actual="$(probe_agent_revision "$agent_path" || true)"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "❌ $driver driver-agent revision 不匹配:asset=$agent_path actual=${actual:-空} expected=$expected"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
echo "✅ $driver driver-agent revision 校验通过:$actual"
|
||||
done
|
||||
|
||||
exit "$failed"
|
||||
Reference in New Issue
Block a user