feat(cli): 新增独立命令行与发布链

- 新增无头运行时及连接、查询、导出、批处理、审计和 MCP 命令
- 复用活动数据根、密文存储与跨进程锁,落实写入安全和取消语义
- 增加六平台 CLI 归档、独立校验和、Docker、npm 与 WinGet 分发
- 隔离 GUI/CLI 更新资产并强化 macOS 签名与公证门禁
- 补充并发、审计、事务及发布契约回归测试

Refs #902
This commit is contained in:
Syngnat
2026-08-11 10:34:58 +08:00
parent ff61a64179
commit 4d5c0e6bb9
87 changed files with 9905 additions and 517 deletions

View File

@@ -27,13 +27,22 @@ jobs:
- name: Test release asset contracts
run: |
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -shellcheck= -pyflakes= \
.github/workflows/release.yml \
.github/workflows/dev-build.yml \
.github/workflows/publish-release.yml
bash tools/detect-changed-driver-agents.test.sh
bash tools/generate-driver-agent-revisions.test.sh
python3 tools/generate-driver-release-manifest.test.py
python3 tools/generate-update-latest-manifest.test.py
python3 tools/validate-gui-update-manifest.test.py
python3 tools/prepare-vps-release-payload.test.py
bash tools/vps-release-commit.test.sh
python3 tools/package-driver-release-assets.test.py
python3 tools/cli-release-assets.test.py
python3 tools/npm-cli-wrapper.test.py
python3 tools/validate-npm-cli-package-version.test.py
python3 tools/generate-winget-cli-manifest.test.py
python3 tools/legal-release-assets.test.py
python3 tools/windows-release-artifacts.test.py
python3 tools/generate-release-notes.test.py
@@ -275,6 +284,85 @@ jobs:
fi
echo "release_source=dev-latest" >> "$GITHUB_OUTPUT"
cli:
name: Build dev CLI ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: darwin
goarch: amd64
extension: tar.gz
binary: gonavi
- goos: darwin
goarch: arm64
extension: tar.gz
binary: gonavi
- goos: linux
goarch: amd64
extension: tar.gz
binary: gonavi
- goos: linux
goarch: arm64
extension: tar.gz
binary: gonavi
- goos: windows
goarch: amd64
extension: zip
binary: gonavi.exe
- goos: windows
goarch: arm64
extension: zip
binary: gonavi.exe
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Build and package dev CLI
shell: bash
run: |
set -euo pipefail
version="dev-${GITHUB_SHA:0:7}"
asset="gonavi-cli_${version}_${{ matrix.goos }}_${{ matrix.goarch }}.${{ matrix.extension }}"
stage="${RUNNER_TEMP}/gonavi-cli-${{ matrix.goos }}-${{ matrix.goarch }}"
mkdir -p "$stage"
./tools/generate-driver-agent-revisions.sh --platform "${{ matrix.goos }}/${{ matrix.goarch }}"
CGO_ENABLED=0 GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" \
go build -trimpath -ldflags="-s -w -X GoNavi-Wails/internal/cli.Version=${version}" \
-o "$stage/${{ matrix.binary }}" ./cmd/gonavi
install -m 0644 LICENSE NOTICE "$stage/"
if [[ "${{ matrix.extension }}" == "zip" ]]; then
(cd "$stage" && zip -q -X "${GITHUB_WORKSPACE}/${asset}" "${{ matrix.binary }}" LICENSE NOTICE)
else
tar -C "$stage" -czf "$asset" "${{ matrix.binary }}" LICENSE NOTICE
fi
test -s "$asset"
expected_entries="$(printf '%s\n' "${{ matrix.binary }}" LICENSE NOTICE | sort)"
if [[ "${{ matrix.extension }}" == "zip" ]]; then
actual_entries="$(unzip -Z1 "$asset" | sort)"
else
actual_entries="$(tar -tzf "$asset" | sed 's#^\./##' | sort)"
fi
if [[ "$actual_entries" != "$expected_entries" ]]; then
echo "CLI archive contents are invalid for $asset" >&2
printf 'expected:\n%s\nactual:\n%s\n' "$expected_entries" "$actual_entries" >&2
exit 1
fi
- name: Upload dev CLI artifact
uses: actions/upload-artifact@v6
with:
name: dev-cli-artifact-${{ matrix.goos }}-${{ matrix.goarch }}
path: gonavi-cli_*
if-no-files-found: error
retention-days: 1
build:
name: Build ${{ matrix.platform }}
needs:
@@ -540,7 +628,7 @@ jobs:
id: version
shell: bash
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
SHORT_SHA="${GITHUB_SHA:0:7}"
DEV_VERSION="dev-${SHORT_SHA}"
echo "version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
echo "📌 Dev 版本号: ${DEV_VERSION}"
@@ -831,12 +919,12 @@ jobs:
VERSION="${{ steps.version.outputs.version }}"
cd build/bin
APP_PATH=$(find . -maxdepth 1 -name "*.app" | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "❌ 未找到 .app 应用包!"
APP_PATH="./GoNavi.app"
if [ ! -d "$APP_PATH" ]; then
echo "❌ 未找到固定名称 GoNavi.app 应用包!"
exit 1
fi
APP_NAME=$(basename "$APP_PATH")
APP_NAME="GoNavi.app"
mkdir -p "$APP_NAME/Contents/Resources"
cp ../../LICENSE "$APP_NAME/Contents/Resources/LICENSE"
cp ../../NOTICE "$APP_NAME/Contents/Resources/NOTICE"
@@ -848,10 +936,7 @@ jobs:
fi
echo " macOS 产物不执行 UPX 压缩,保留原始主程序。"
echo "🔏 正在进行 Ad-hoc 签名..."
if command -v xattr >/dev/null 2>&1; then
xattr -cr "$APP_NAME" || true
fi
echo "🔏 Signing dev DMG with an ad-hoc identity (not for production distribution)..."
codesign --force --deep --sign - "$APP_NAME"
DMG_NAME="${{ matrix.build_name }}.dmg"
@@ -871,9 +956,9 @@ jobs:
VERIFY_MOUNT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gonavi-dev-verify.XXXXXX")
hdiutil attach -nobrowse -readonly -mountpoint "$VERIFY_MOUNT_DIR" "$DMG_NAME" >/dev/null
PACKAGED_APP=$(find "$VERIFY_MOUNT_DIR" -maxdepth 1 -name "*.app" | head -n 1)
if [ -z "$PACKAGED_APP" ]; then
echo "❌ DMG 内未找到 .app 应用包!"
PACKAGED_APP="$VERIFY_MOUNT_DIR/GoNavi.app"
if [ ! -d "$PACKAGED_APP" ]; then
echo "❌ DMG 内未找到固定名称 GoNavi.app 应用包!"
hdiutil detach "$VERIFY_MOUNT_DIR" -quiet >/dev/null 2>&1 || true
exit 1
fi
@@ -1093,6 +1178,7 @@ jobs:
name: Publish Dev Pre-release
needs:
- build
- cli
- driver_agents
# Serialize only the publication stage with stable releases so both
# channels cannot publish shared mirror targets concurrently.
@@ -1119,12 +1205,44 @@ jobs:
pattern: dev-build-artifacts-*
merge-multiple: true
- name: Download dev CLI artifacts
uses: actions/download-artifact@v7
with:
path: cli-assets
pattern: dev-cli-artifact-*
merge-multiple: true
- name: Validate dev CLI artifact staging
shell: bash
run: |
set -euo pipefail
version="dev-${GITHUB_SHA:0:7}"
expected=(
"gonavi-cli_${version}_darwin_amd64.tar.gz"
"gonavi-cli_${version}_darwin_arm64.tar.gz"
"gonavi-cli_${version}_linux_amd64.tar.gz"
"gonavi-cli_${version}_linux_arm64.tar.gz"
"gonavi-cli_${version}_windows_amd64.zip"
"gonavi-cli_${version}_windows_arm64.zip"
)
mapfile -t actual < <(find cli-assets -type f -printf '%P\n' | sort)
expected_list="$(printf '%s\n' "${expected[@]}" | sort)"
actual_list="$(printf '%s\n' "${actual[@]}")"
if [[ "$actual_list" != "$expected_list" ]]; then
echo "CLI artifact set is invalid" >&2
printf 'expected:\n%s\nactual:\n%s\n' "$expected_list" "$actual_list" >&2
exit 1
fi
for asset in "${expected[@]}"; do
test -s "cli-assets/${asset}"
done
- name: Add legal documents
shell: bash
run: install -m 0644 LICENSE NOTICE release-assets/
- name: List Assets
run: ls -R release-assets
run: ls -R release-assets cli-assets
- name: Download Previous Driver Manifest
if: needs.driver_agents.outputs.has_changes == 'true' && needs.driver_agents.outputs.release_source != 'all'
@@ -1190,26 +1308,107 @@ jobs:
rm -rf drivers driver-provenance
echo "has_driver_assets=true" >> "$GITHUB_OUTPUT"
- name: Generate dev CLI checksums
shell: bash
run: |
set -euo pipefail
version="dev-${GITHUB_SHA:0:7}"
expected=(
"gonavi-cli_${version}_darwin_amd64.tar.gz"
"gonavi-cli_${version}_darwin_arm64.tar.gz"
"gonavi-cli_${version}_linux_amd64.tar.gz"
"gonavi-cli_${version}_linux_arm64.tar.gz"
"gonavi-cli_${version}_windows_amd64.zip"
"gonavi-cli_${version}_windows_arm64.zip"
)
(cd cli-assets && sha256sum "${expected[@]}" > "gonavi-cli_${version}_checksums.txt")
- name: Generate SHA256SUMS
shell: bash
run: |
cd release-assets
FILES=()
set -euo pipefail
output="release-assets/SHA256SUMS"
count=0
declare -A seen=()
: > "$output"
while IFS= read -r file; do
if [ -n "$file" ]; then
case "$file" in
SHA256SUMS|latest.json|latest-dev.json) continue ;;
esac
FILES+=("$file")
name="$(basename "$file")"
if [[ -n "${seen[$name]:-}" ]]; then
echo "Duplicate release asset name across staging directories: ${name}" >&2
exit 1
fi
done < <(find . -maxdepth 1 -type f ! -name SHA256SUMS -exec basename {} \; | sort)
if [ ${#FILES[@]} -eq 0 ]; then
seen["$name"]=1
digest="$(sha256sum "$file" | awk '{ print $1 }')"
printf '%s %s\n' "$digest" "$name" >> "$output"
count=$((count + 1))
done < <(find release-assets cli-assets -maxdepth 1 -type f \
! -name SHA256SUMS ! -name latest.json ! -name latest-dev.json -print | sort)
if [ "$count" -eq 0 ]; then
echo "⚠️ 未找到可签名资产,生成空 SHA256SUMS"
: > SHA256SUMS
else
sha256sum "${FILES[@]}" > SHA256SUMS
fi
- name: Verify dev CLI release assets
shell: bash
run: |
set -euo pipefail
version="dev-${GITHUB_SHA:0:7}"
cli_checksum_name="gonavi-cli_${version}_checksums.txt"
expected=(
"gonavi-cli_${version}_darwin_amd64.tar.gz"
"gonavi-cli_${version}_darwin_arm64.tar.gz"
"gonavi-cli_${version}_linux_amd64.tar.gz"
"gonavi-cli_${version}_linux_arm64.tar.gz"
"gonavi-cli_${version}_windows_amd64.zip"
"gonavi-cli_${version}_windows_arm64.zip"
)
expected_cli_files=("${expected[@]}" "$cli_checksum_name")
mapfile -t actual_cli_files < <(find cli-assets -maxdepth 1 -type f -name 'gonavi-cli_*' -printf '%f\n' | sort)
expected_cli_list="$(printf '%s\n' "${expected_cli_files[@]}" | sort)"
actual_cli_list="$(printf '%s\n' "${actual_cli_files[@]}")"
if [[ "$actual_cli_list" != "$expected_cli_list" ]]; then
echo "CLI release asset set is invalid" >&2
printf 'expected:\n%s\nactual:\n%s\n' "$expected_cli_list" "$actual_cli_list" >&2
exit 1
fi
test -s "cli-assets/${cli_checksum_name}"
if ! awk 'NF != 2 || length($1) != 64 || $1 !~ /^[0-9a-fA-F]+$/ { exit 1 }' "cli-assets/${cli_checksum_name}"; then
echo "CLI checksum file contents are invalid" >&2
exit 1
fi
mapfile -t checksum_assets < <(awk '{ print $2 }' "cli-assets/${cli_checksum_name}" | sort)
checksum_asset_list="$(printf '%s\n' "${checksum_assets[@]}")"
expected_checksum_list="$(printf '%s\n' "${expected[@]}" | sort)"
if [[ "$checksum_asset_list" != "$expected_checksum_list" ]]; then
echo "CLI checksum file contents are invalid" >&2
printf 'expected:\n%s\nactual:\n%s\n' "$expected_checksum_list" "$checksum_asset_list" >&2
exit 1
fi
for asset in "${expected[@]}"; do
test -s "cli-assets/${asset}"
grep -Fq " ${asset}" release-assets/SHA256SUMS
global_digest="$(awk -v name="$asset" '$2 == name { print $1 }' release-assets/SHA256SUMS)"
cli_digest="$(awk -v name="$asset" '$2 == name { print $1 }' "cli-assets/${cli_checksum_name}")"
actual_digest="$(sha256sum "cli-assets/${asset}" | awk '{ print $1 }')"
[[ -n "$global_digest" && "$global_digest" == "$cli_digest" && "$cli_digest" == "$actual_digest" ]]
done
grep -Fq " ${cli_checksum_name}" release-assets/SHA256SUMS
(cd cli-assets && sha256sum --check "$cli_checksum_name")
while read -r expected_digest asset; do
if [[ -f "release-assets/${asset}" && -f "cli-assets/${asset}" ]]; then
echo "Duplicate release asset name across staging directories: ${asset}" >&2
exit 1
elif [[ -f "release-assets/${asset}" ]]; then
file="release-assets/${asset}"
elif [[ -f "cli-assets/${asset}" ]]; then
file="cli-assets/${asset}"
else
echo "Release asset listed in SHA256SUMS is missing: ${asset}" >&2
exit 1
fi
actual_digest="$(sha256sum "$file" | awk '{ print $1 }')"
[[ "$actual_digest" == "$expected_digest" ]]
done < release-assets/SHA256SUMS
- name: Generate Driver SHA256SUMS
if: steps.driver_assets.outputs.has_driver_assets == 'true'
shell: bash
@@ -1347,6 +1546,7 @@ jobs:
--version "$DEV_VERSION" \
--tag dev-latest \
--channel dev \
--component gui \
--name "Dev Build (${DEV_VERSION})" \
--download-base-url "https://download.syngnat.top/gonavi/dev/releases/download" \
--download-tag "$DEV_VERSION" \
@@ -1482,7 +1682,9 @@ jobs:
tag_name: dev-latest
name: "🧪 Dev Build (${{ steps.version.outputs.version }})"
target_commitish: ${{ github.sha }}
files: release-assets/*
files: |
release-assets/*
cli-assets/*
prerelease: true
draft: false
body_path: ${{ steps.changelog.outputs.body_file }}
@@ -1499,21 +1701,12 @@ jobs:
app_dir="release-assets"
driver_dir="driver-release-assets"
jq -e \
--arg version "${DEV_VERSION}" \
--arg mirror_base "https://download.syngnat.top/gonavi/dev/releases/download" \
--arg github_base "https://github.com/${GITHUB_REPOSITORY}/releases/download/dev-latest" \
'
.channel == "dev"
and .tagName == "dev-latest"
and .version == $version
and (.assets | type == "array" and length > 0)
and all(.assets[];
(.url | type == "string" and startswith($mirror_base + "/" + $version + "/"))
and (.apiUrl | type == "string" and startswith($github_base + "/"))
and (.sha256 | type == "string" and test("^[0-9a-fA-F]{64}$"))
)
' "${app_dir}/latest-dev.json" >/dev/null
python3 tools/validate-gui-update-manifest.py \
--channel dev \
--app-tag "${DEV_VERSION}" \
--app-dir "${app_dir}" \
--manifest "${app_dir}/latest-dev.json" \
--github-repository "${GITHUB_REPOSITORY}"
driver_latest_file="${RUNNER_TEMP}/driver-dev-latest-index.json"
if [[ "${HAS_DRIVER_ASSETS}" == "true" ]]; then