Files
navop/.github/workflows/release.yml
Workflow config file is invalid. Please check your config file: getMatrixes: matrix include must be a list of mappings
胡飞 1712e6ffe1 ci: sccache 安装失败时退化为无缓存构建,不再让 job 失败
mozilla-actions/sccache-action 从 GitHub Release CDN 下载 sccache 二进制失败
(CI run 35347089674 上连续两次 HTTP 504)会直接让整个 test job 变红,
Windows 测试根本没跑到,ci-gate 随之拦截合并。sccache 只是构建加速器,
不应成为测试或发布 job 的可用性依赖。

- Setup sccache 标记为 continue-on-error: true;该 action 的 post step 复用同一个
  Pipelines.ActionStep,因此继续性错误同样覆盖 post step 失败
- Initialize sccache 在 steps.sccache.outcome != 'success' 时输出 warning,并向
  GITHUB_ENV 写入空 RUSTC_WRAPPER,让后续 cargo 走无缓存构建;空值与 ci.yml
  中 windows-rdp-probe job 的 RUSTC_WRAPPER: "" 是同一语义
- 同步 release.yml 的同一段路径,避免发布 job 被同类瞬时故障打断
- script/test-release-packaging.mjs 的共享缓存策略契约补上 best-effort 断言,
  防止后续改动把这两个步骤改回硬依赖

验证:
- ruby -ryaml 解析两个 workflow:jobs 与步骤结构不变
- node --test script/test-release-packaging.mjs:26 passed / 0 failed
- cargo test -p windows_rdp_host --test contract:39 passed / 0 failed
- 变异验证:临时删掉 ci.yml 的 continue-on-error,共享缓存策略契约立即转红
- 本地 RUSTC_WRAPPER="" cargo check 正常,确认空值等价于不启用 wrapper
2026-09-19 00:10:02 +08:00

855 lines
34 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Release
on:
workflow_dispatch:
inputs:
tag:
description: "GitHub Release tag to build or repair, for example v0.8.10"
required: true
type: string
platform:
description: "Build all primary platforms or repair one platform"
required: true
type: choice
default: all
options:
- all
- macos-arm64
- macos-x64
- linux-x64
- linux-x64-portable
- linux-arm64
- linux-arm64-portable
- windows-x64
- windows-x86
build_mode:
description: "fast 用 thin LTO(构建较快、体积略大);release 用 fat LTO + panic=abort(体积最小)"
required: true
type: choice
default: release
options:
- release
- fast
permissions:
actions: write
contents: write
concurrency:
group: release-${{ inputs.tag }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
SCCACHE_IDLE_TIMEOUT: "7200"
jobs:
prepare:
name: Prepare release build
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
tag: ${{ steps.release.outputs.tag }}
platform: ${{ steps.release.outputs.platform }}
build_mode: ${{ steps.release.outputs.build_mode }}
steps:
- name: Validate release request
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_PLATFORM: ${{ inputs.platform }}
BUILD_MODE: ${{ inputs.build_mode }}
run: |
set -euo pipefail
if [ "$BUILD_MODE" != "release" ] && [ "$BUILD_MODE" != "fast" ]; then
echo "::error::Unsupported build mode: $BUILD_MODE"
exit 1
fi
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid release tag: $RELEASE_TAG"
exit 1
fi
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" >/dev/null
if [ "$RELEASE_PLATFORM" != "all" ]; then
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
fi
{
echo "tag=$RELEASE_TAG"
echo "platform=$RELEASE_PLATFORM"
echo "build_mode=$BUILD_MODE"
} >> "$GITHUB_OUTPUT"
- name: Check out release tag
uses: actions/checkout@v7
with:
ref: ${{ steps.release.outputs.tag }}
- name: Verify application version matches release tag
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
expected_version="${RELEASE_TAG#v}"
manifest_version="$(
awk -F'"' '
/^\[package\]$/ { in_package = 1; next }
/^\[/ { in_package = 0 }
in_package && /^version = "/ { print $2; exit }
' main/Cargo.toml
)"
if [ "$manifest_version" != "$expected_version" ]; then
echo "::error::main/Cargo.toml version ${manifest_version:-<missing>} does not match release tag ${RELEASE_TAG}"
exit 1
fi
echo "Verified Navop application version: $manifest_version"
- name: Verify changelog entry
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
if [ -f CHANGELOG.md ] && [ -f script/changelog.py ]; then
python3 script/changelog.py extract \
--tag "$RELEASE_TAG" \
--changelog CHANGELOG.md \
--output /tmp/release-notes.md
echo "Verified tagged changelog entry: $RELEASE_TAG"
elif gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::warning::${RELEASE_TAG} predates the tracked changelog; preserving its existing GitHub Release body for this legacy repair."
else
echo "::error::${RELEASE_TAG} does not contain CHANGELOG.md and script/changelog.py. Generate and commit the changelog entry before creating a new release tag."
exit 1
fi
- name: Resolve platform matrix
id: matrix
env:
RELEASE_PLATFORM: ${{ inputs.platform }}
run: |
set -euo pipefail
macos_arm64='{"target":"aarch64-apple-darwin","os":"macos-latest","binary":"navop","archive":"navop-aarch64-apple-darwin.tar.gz","public_label":"macos-arm64","variant":"standard","arm_linux":false,"windows_native_rdp":false,"portable_linux":false}'
macos_x64='{"target":"x86_64-apple-darwin","os":"macos-15-intel","binary":"navop","archive":"navop-x86_64-apple-darwin.tar.gz","public_label":"macos-x64","variant":"standard","arm_linux":false,"windows_native_rdp":false,"portable_linux":false}'
linux_x64='{"target":"x86_64-unknown-linux-gnu","os":"ubuntu-latest","binary":"navop","archive":"navop-x86_64-unknown-linux-gnu.tar.gz","public_label":"linux-x64","variant":"standard","arm_linux":false,"windows_native_rdp":false,"portable_linux":false}'
linux_x64_portable='{"target":"x86_64-unknown-linux-gnu","os":"ubuntu-22.04","binary":"navop","archive":"navop-x86_64-unknown-linux-gnu-portable.tar.gz","public_label":"linux-x64-portable","variant":"portable","arm_linux":false,"windows_native_rdp":false,"portable_linux":true}'
linux_arm64='{"target":"aarch64-unknown-linux-gnu","os":"ubuntu-24.04-arm","binary":"navop","archive":"navop-aarch64-unknown-linux-gnu.tar.gz","public_label":"linux-arm64","variant":"standard","arm_linux":true,"windows_native_rdp":false,"portable_linux":false}'
linux_arm64_portable='{"target":"aarch64-unknown-linux-gnu","os":"ubuntu-24.04-arm","binary":"navop","archive":"navop-aarch64-unknown-linux-gnu-portable.tar.gz","public_label":"linux-arm64-portable","variant":"portable","arm_linux":true,"windows_native_rdp":false,"portable_linux":true}'
windows_x64='{"target":"x86_64-pc-windows-msvc","os":"windows-latest","binary":"navop.exe","archive":"navop-x86_64-pc-windows-msvc.zip","public_label":"windows-x64","variant":"standard","arm_linux":false,"windows_native_rdp":true,"portable_linux":false,"windows_arch":"x64"}'
windows_x86='{"target":"i686-pc-windows-msvc","os":"windows-latest","binary":"navop.exe","archive":"navop-i686-pc-windows-msvc.zip","public_label":"win32","variant":"standard","arm_linux":false,"windows_native_rdp":true,"portable_linux":false,"windows_arch":"x86"}'
case "$RELEASE_PLATFORM" in
all) matrix="[$macos_arm64,$macos_x64,$linux_x64,$linux_x64_portable,$linux_arm64,$linux_arm64_portable,$windows_x64,$windows_x86]" ;;
macos-arm64) matrix="[$macos_arm64]" ;;
macos-x64) matrix="[$macos_x64]" ;;
linux-x64) matrix="[$linux_x64,$linux_x64_portable]" ;;
linux-x64-portable) matrix="[$linux_x64_portable]" ;;
linux-arm64) matrix="[$linux_arm64,$linux_arm64_portable]" ;;
linux-arm64-portable) matrix="[$linux_arm64_portable]" ;;
windows-x64) matrix="[$windows_x64]" ;;
windows-x86) matrix="[$windows_x86]" ;;
*) echo "::error::Unsupported platform: $RELEASE_PLATFORM"; exit 1 ;;
esac
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
build:
name: Build (${{ matrix.target }} / ${{ matrix.variant }})
needs: prepare
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.prepare.outputs.matrix) }}
runs-on: ${{ matrix.os }}
# fat LTO + codegen-units=1 让链接阶段更慢(Windows x64 基线已 64min),一律给 180min
timeout-minutes: 180
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
cache: false
# sccache 只是构建加速器:GitHub Release CDN 偶发 504 会让下载安装失败,
# 此时必须退化成无缓存构建,而不是让整个发布 job 失败。
- name: Setup sccache
id: sccache
uses: mozilla-actions/sccache-action@v0.0.10
continue-on-error: true
- name: Initialize sccache
shell: bash
run: |
set -euo pipefail
if [ "${{ steps.sccache.outcome }}" != "success" ]; then
echo "::warning::sccache 安装失败,本次构建退化为无编译器缓存"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
exit 0
fi
sccache --start-server
sccache --zero-stats
sccache --show-stats
- name: Restore shared Cargo inputs
id: cargo-inputs-cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: navop-cargo-inputs-v1-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
navop-cargo-inputs-v1-${{ runner.os }}-
release-cargo-inputs-${{ runner.os }}-${{ matrix.target }}-
- name: Free disk space (macOS)
if: runner.os == 'macOS'
run: |
df -h
remove_path() {
local path="$1"
if [ -e "$path" ]; then
sudo rm -rf "$path" || echo "::warning::Failed to remove $path"
fi
}
remove_directory_contents() {
local path="$1"
if [ -d "$path" ]; then
sudo find "$path" -mindepth 1 -maxdepth 1 -exec rm -rf {} + \
|| echo "::warning::Failed to remove some entries under $path"
fi
}
remove_path /usr/local/lib/node_modules
remove_path "$HOME/.npm"
remove_path "$HOME/.nvm"
remove_directory_contents "$HOME/Library/Caches"
remove_path /System/Library/Frameworks/Python.framework
df -h
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: script/bootstrap
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: script/bootstrap
- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cmake --version
choco install nasm --no-progress --yes
$nasmDir = Join-Path $env:ProgramFiles "NASM"
$nasmExe = Join-Path $nasmDir "nasm.exe"
if (-not (Test-Path -LiteralPath $nasmExe -PathType Leaf)) {
throw "NASM executable was not found at '$nasmExe'"
}
$env:Path = "$nasmDir;$env:Path"
$nasmDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
nasm -v
- name: Install Zig toolchain (portable Linux)
if: matrix.portable_linux
shell: bash
run: |
set -euo pipefail
python3 -m venv "$RUNNER_TEMP/ziglang"
"$RUNNER_TEMP/ziglang/bin/python" -m pip install ziglang==0.14.1
cargo install --locked cargo-zigbuild --version 0.23.0
echo "CARGO_ZIGBUILD_PYTHON_PATH=$RUNNER_TEMP/ziglang/bin/python" >> "$GITHUB_ENV"
"$RUNNER_TEMP/ziglang/bin/python" -m ziglang version
cargo-zigbuild --version
- name: Install portable packaging dependencies
if: matrix.portable_linux
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y binutils musl-tools
- name: Configure MSVC environment
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.windows_arch }}
- name: Build Windows RDP toolchain probe
if: runner.os == 'Windows'
shell: pwsh
run: >-
./script/build-windows-rdp-probe.ps1
-Target "${{ matrix.target }}"
- name: Set environment variables
shell: bash
run: |
VERSION="${{ needs.prepare.outputs.tag }}"
echo "ONETCLI_VERSION=${VERSION#v}" >> "$GITHUB_ENV"
echo "PUBLIC_BASENAME=navop-${VERSION#v}-${{ matrix.public_label }}" >> "$GITHUB_ENV"
- name: Verify secrets are set
shell: bash
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
run: |
if [ -z "$SUPABASE_URL" ]; then
echo "::error::SUPABASE_URL secret is empty or not set"
exit 1
fi
if [ -z "$SUPABASE_ANON_KEY" ]; then
echo "::error::SUPABASE_ANON_KEY secret is empty or not set"
exit 1
fi
- name: Build release binary
if: runner.os != 'Windows'
shell: bash
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
NAVOP_PUBLIC_BASE_URL: ${{ vars.NAVOP_PUBLIC_BASE_URL }}
NAVOP_WEBSITE_BASE_URL: ${{ vars.NAVOP_WEBSITE_BASE_URL }}
ONETCLI_TEAM_MANAGEMENT_URL_TEMPLATE: ${{ vars.ONETCLI_TEAM_MANAGEMENT_URL_TEMPLATE }}
run: |
set -euo pipefail
if [ "${{ needs.prepare.outputs.build_mode }}" = "fast" ]; then
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
elif [ "${{ matrix.arm_linux }}" = "true" ]; then
export CARGO_BUILD_JOBS=2
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
fi
cargo --version
rustc --version
echo "CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS:-}"
echo "CARGO_PROFILE_RELEASE_LTO=${CARGO_PROFILE_RELEASE_LTO:-}"
echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${CARGO_PROFILE_RELEASE_CODEGEN_UNITS:-}"
if [ "${{ matrix.portable_linux }}" = "true" ]; then
cargo zigbuild \
--release \
-p main \
--target "${{ matrix.target }}.2.28" \
--no-default-features \
--features wasm-components,shell-plugins
else
cargo build --release -p main --target "${{ matrix.target }}"
fi
test -x "target/${{ matrix.target }}/release/${{ matrix.binary }}"
# 体积回归在日志里可见(fat LTO + panic=abort 后约 107MiB
ls -lh "target/${{ matrix.target }}/release/${{ matrix.binary }}"
- name: Build release binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
NAVOP_PUBLIC_BASE_URL: ${{ vars.NAVOP_PUBLIC_BASE_URL }}
NAVOP_WEBSITE_BASE_URL: ${{ vars.NAVOP_WEBSITE_BASE_URL }}
ONETCLI_TEAM_MANAGEMENT_URL_TEMPLATE: ${{ vars.ONETCLI_TEAM_MANAGEMENT_URL_TEMPLATE }}
run: |
if ([string]::IsNullOrWhiteSpace($env:VCToolsInstallDir)) {
throw "VCToolsInstallDir is not configured"
}
$target = "${{ matrix.target }}"
$linkerArchitecture = if ($target -eq "i686-pc-windows-msvc") {
"x86"
} else {
"x64"
}
$linker = Join-Path `
$env:VCToolsInstallDir `
"bin\Hostx64\$linkerArchitecture\link.exe"
if (-not (Test-Path -LiteralPath $linker -PathType Leaf)) {
throw "MSVC linker was not found at '$linker'"
}
$linkerEnvironmentName = "CARGO_TARGET_$($target.Replace('-', '_').ToUpperInvariant())_LINKER"
[Environment]::SetEnvironmentVariable(
$linkerEnvironmentName,
$linker,
[EnvironmentVariableTarget]::Process
)
cargo --version
rustc --version
Write-Host "Using MSVC linker for ${target}: $linker"
if ("${{ needs.prepare.outputs.build_mode }}" -eq "fast") {
$env:CARGO_PROFILE_RELEASE_LTO = "thin"
$env:CARGO_PROFILE_RELEASE_CODEGEN_UNITS = "16"
Write-Host "Fast build mode: thin LTO, codegen-units=16"
}
# 32 位 Windows 暂不支持 shell-plugins:其 gpui-shell 依赖的
# quickjs-jit 运行时代码与 cranelift 后端仅支持 64 位
# (x86_64/aarch64)i686 无法编译。win32 构建先剔除该 feature。
$features = if ($target -eq "i686-pc-windows-msvc") {
@("--no-default-features", "--features", "wasm-components,embedded-webview,windows-native-rdp")
} else {
@("--features", "windows-native-rdp")
}
cargo build `
--release `
-p main `
@features `
--target $target
$binary = "target\$target\release\${{ matrix.binary }}"
if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) {
throw "Release binary was not created: '$binary'"
}
- name: Verify portable Linux glibc baseline
if: matrix.portable_linux
shell: bash
run: |
set -euo pipefail
script/check-linux-glibc-baseline.sh \
"target/${{ matrix.target }}/release/${{ matrix.binary }}" \
"2.28"
- name: Save shared Cargo inputs
if: >-
${{
always() &&
steps.cargo-inputs-cache.outcome == 'success' &&
steps.cargo-inputs-cache.outputs.cache-hit != 'true' &&
(
needs.prepare.outputs.platform != 'all' ||
matrix.target == 'x86_64-apple-darwin' ||
matrix.target == 'x86_64-unknown-linux-gnu' ||
matrix.target == 'x86_64-pc-windows-msvc'
)
}}
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ steps.cargo-inputs-cache.outputs.cache-primary-key }}
- name: Show sccache statistics
if: always()
shell: bash
run: sccache --show-stats || true
- name: Create macOS app bundle
if: runner.os == 'macOS'
run: |
chmod +x script/bundle-macos.sh
chmod +x script/bundle-macos-dmg.sh
script/bundle-macos.sh ${{ matrix.target }}
# 释放磁盘:release target 目录很大,DMG 制作(在 target/ 下写入镜像)
# 会因 runner 空间不足而失败(hdiutil: No space left on device)。
df -h
rm -rf target/release/deps target/release/build target/release/incremental target/release/.fingerprint
rm -rf "target/${{ matrix.target }}/release/deps" "target/${{ matrix.target }}/release/build" "target/${{ matrix.target }}/release/incremental" "target/${{ matrix.target }}/release/.fingerprint"
df -h
script/bundle-macos-dmg.sh ${{ matrix.target }}
tar czf ${{ matrix.archive }} -C target Navop.app
cp "${{ matrix.archive }}" "${PUBLIC_BASENAME}.tar.gz"
mv "navop-${{ matrix.target }}.dmg" "${PUBLIC_BASENAME}.dmg"
- name: Package (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
rm -rf package
if [ "${{ matrix.portable_linux }}" = "true" ]; then
script/package-linux-portable.sh \
--binary "target/${{ matrix.target }}/release/${{ matrix.binary }}" \
--output package \
--launcher-source script/linux-portable-launcher.c \
--target "${{ matrix.target }}" \
--glibc-baseline "2.28"
else
mkdir -p package/usr/bin
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" package/usr/bin/
fi
mkdir -p package/usr/share/applications
mkdir -p package/usr/share/mime/packages
mkdir -p package/usr/share/icons/hicolor/128x128/apps
mkdir -p package/usr/share/icons/hicolor/256x256/apps
mkdir -p package/usr/share/icons/hicolor/512x515/apps
cp resources/linux/navop.desktop package/usr/share/applications/
cp resources/linux/navop.xml package/usr/share/mime/packages/
cp resources/linux/navop-128.png package/usr/share/icons/hicolor/128x128/apps/navop.png
cp resources/linux/navop-256.png package/usr/share/icons/hicolor/256x256/apps/navop.png
cp resources/linux/navop-512.png package/usr/share/icons/hicolor/512x515/apps/navop.png
tar \
--sort=name \
--mtime='UTC 1970-01-01' \
--owner=0 \
--group=0 \
--numeric-owner \
-czf "${{ matrix.archive }}" \
-C package .
cp "${{ matrix.archive }}" "${PUBLIC_BASENAME}.tar.gz"
- name: Install Linux packaging dependencies
if: matrix.target == 'x86_64-unknown-linux-gnu' && !matrix.portable_linux
run: |
sudo apt-get update
sudo apt-get install -y rpm
- name: Package Linux installers (x86_64)
if: matrix.target == 'x86_64-unknown-linux-gnu' && !matrix.portable_linux
shell: bash
run: |
set -euo pipefail
PACKAGE_VERSION="${ONETCLI_VERSION//-/.}"
DEB_NAME="navop_${PACKAGE_VERSION}_amd64.deb"
RPM_NAME="navop-${PACKAGE_VERSION}-1.x86_64.rpm"
APPIMAGE_NAME="navop_${PACKAGE_VERSION}_amd64.AppImage"
rm -rf package-deb rpmbuild Navop.AppDir linuxdeploy-x86_64.AppImage
mkdir -p package-deb/DEBIAN
cp -a package/. package-deb/
cat > package-deb/DEBIAN/control <<EOF
Package: navop
Version: ${PACKAGE_VERSION}
Section: utils
Priority: optional
Architecture: amd64
Provides: onetcli
Replaces: onetcli
Conflicts: onetcli
Maintainer: feigeCode <noreply@github.com>
Homepage: https://github.com/feigeCode/navop
Description: Navop - Database, SSH, Terminal, AI Tools
EOF
cat > package-deb/DEBIAN/postinst <<'EOF'
#!/bin/sh
set -e
if command -v update-mime-database >/dev/null 2>&1; then
update-mime-database /usr/share/mime
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database /usr/share/applications
fi
EOF
cat > package-deb/DEBIAN/postrm <<'EOF'
#!/bin/sh
set -e
if command -v update-mime-database >/dev/null 2>&1; then
update-mime-database /usr/share/mime
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database /usr/share/applications
fi
EOF
chmod 0755 package-deb/DEBIAN/postinst package-deb/DEBIAN/postrm
dpkg-deb --root-owner-group --build package-deb "${DEB_NAME}"
RPM_ROOT="${PWD}/rpmbuild"
mkdir -p "${RPM_ROOT}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
tar czf "${RPM_ROOT}/SOURCES/navop-${PACKAGE_VERSION}.tar.gz" -C package .
cat > "${RPM_ROOT}/SPECS/navop.spec" <<EOF
Name: navop
Version: ${PACKAGE_VERSION}
Release: 1%{?dist}
Summary: Navop - Database, SSH, Terminal, AI Tools
License: Apache-2.0
URL: https://github.com/feigeCode/navop
BuildArch: x86_64
Provides: onetcli = %{version}-%{release}
Obsoletes: onetcli < %{version}-%{release}
Source0: %{name}-%{version}.tar.gz
%description
Navop - Database, SSH, Terminal, AI Tools.
%prep
mkdir -p %{_builddir}/%{name}-%{version}
tar -xzf %{SOURCE0} -C %{_builddir}/%{name}-%{version}
%build
%install
mkdir -p %{buildroot}
cp -a %{_builddir}/%{name}-%{version}/* %{buildroot}/
%post
update-mime-database /usr/share/mime >/dev/null 2>&1 || :
update-desktop-database /usr/share/applications >/dev/null 2>&1 || :
%postun
update-mime-database /usr/share/mime >/dev/null 2>&1 || :
update-desktop-database /usr/share/applications >/dev/null 2>&1 || :
%files
/usr/bin/navop
/usr/share/applications/navop.desktop
/usr/share/mime/packages/navop.xml
/usr/share/icons/hicolor/128x128/apps/navop.png
/usr/share/icons/hicolor/256x256/apps/navop.png
/usr/share/icons/hicolor/512x515/apps/navop.png
EOF
rpmbuild --define "_topdir ${RPM_ROOT}" -bb "${RPM_ROOT}/SPECS/navop.spec"
generated_rpm="$(find "${RPM_ROOT}/RPMS" -name "navop-${PACKAGE_VERSION}-1*.x86_64.rpm" -print -quit)"
test -n "${generated_rpm}"
cp "${generated_rpm}" "${RPM_NAME}"
mkdir -p Navop.AppDir
cp -a package/. Navop.AppDir/
curl -L -o linuxdeploy-x86_64.AppImage \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
VERSION="${PACKAGE_VERSION}" APPIMAGE_EXTRACT_AND_RUN=1 ./linuxdeploy-x86_64.AppImage \
--appdir Navop.AppDir \
--executable Navop.AppDir/usr/bin/navop \
--desktop-file resources/linux/navop.desktop \
--icon-file package/usr/share/icons/hicolor/512x515/apps/navop.png \
--output appimage
generated_appimage="$(find . -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy-x86_64.AppImage' -print -quit)"
test -n "${generated_appimage}"
mv "${generated_appimage}" "${APPIMAGE_NAME}"
- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force package | Out-Null
New-Item -ItemType Directory -Force portable-package | Out-Null
Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary }}" "package/${{ matrix.binary }}"
Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary }}" "portable-package/${{ matrix.binary }}"
Set-Content -Path "portable-package/navop.portable" -Value "" -NoNewline
Compress-Archive -Path "package/*" -DestinationPath "${{ matrix.archive }}"
Copy-Item "${{ matrix.archive }}" "${env:PUBLIC_BASENAME}.zip"
Compress-Archive -Path "portable-package/*" -DestinationPath "${env:PUBLIC_BASENAME}-portable.zip"
- name: Install WiX Toolset
if: runner.os == 'Windows'
shell: pwsh
run: |
node script/generate-windows-license.mjs
dotnet tool install --global wix --version 6.0.2
wix extension add -g WixToolset.UI.wixext/6.0.2
wix extension add -g WixToolset.BootstrapperApplications.wixext/6.0.2
- name: Build Windows installers
if: runner.os == 'Windows'
shell: pwsh
run: |
$msiVersion = ($env:ONETCLI_VERSION -split '-')[0]
if ($msiVersion -notmatch '^\d+\.\d+\.\d+$') {
$msiVersion = '0.0.0'
}
wix build installer/windows/navop.wxs `
-ext WixToolset.UI.wixext `
-culture en-US `
-loc installer/windows/navop.wxl `
-arch ${{ matrix.windows_arch }} `
-d Version="$msiVersion" `
-d SourceDir="${{ github.workspace }}\package" `
-d IconPath="${{ github.workspace }}\resources\windows\navop.ico" `
-out "${env:PUBLIC_BASENAME}.msi"
wix build installer/windows/navop-bundle.wxs `
-ext WixToolset.BootstrapperApplications.wixext `
-arch ${{ matrix.windows_arch }} `
-d Version="$msiVersion" `
-d MsiPath="${{ github.workspace }}\${env:PUBLIC_BASENAME}.msi" `
-d IconPath="${{ github.workspace }}\resources\windows\navop.ico" `
-out "${env:PUBLIC_BASENAME}.exe"
- name: Validate Windows MSI metadata
if: runner.os == 'Windows'
shell: pwsh
run: |
./script/validate-windows-msi.ps1 `
-Path "${env:PUBLIC_BASENAME}.msi" `
-ExpectedLanguage 1033
- name: Upload artifact
if: matrix.variant == 'standard'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.archive }}
path: ${{ matrix.archive }}
- name: Upload versioned public package artifacts
uses: actions/upload-artifact@v4
with:
name: navop-${{ matrix.public_label }}-packages
path: |
navop-*-${{ matrix.public_label }}.tar.gz
navop-*-${{ matrix.public_label }}.dmg
navop-*-${{ matrix.public_label }}.zip
navop-*-${{ matrix.public_label }}-portable.zip
navop-*-${{ matrix.public_label }}.msi
navop-*-${{ matrix.public_label }}.exe
if-no-files-found: error
- name: Upload Linux installer artifacts
if: matrix.target == 'x86_64-unknown-linux-gnu' && !matrix.portable_linux
uses: actions/upload-artifact@v4
with:
name: navop-linux-x86_64-installers
path: |
navop_*.deb
navop-*.rpm
navop_*.AppImage
release:
name: Publish Release
needs:
- prepare
- build
runs-on: ubuntu-latest
steps:
- name: Check out release tag
uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
- name: Extract release notes from changelog
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
if [ -f CHANGELOG.md ] && [ -f script/changelog.py ]; then
python3 script/changelog.py extract \
--tag "$RELEASE_TAG" \
--changelog CHANGELOG.md \
--output release-notes.md
elif gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::warning::${RELEASE_TAG} predates the tracked changelog; using its existing GitHub Release body for this legacy repair."
gh release view "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--json body \
--jq .body > release-notes.md
else
echo "::error::Cannot publish ${RELEASE_TAG}: the tag has no tracked changelog entry and no legacy GitHub Release body."
exit 1
fi
if [ ! -s release-notes.md ]; then
echo "::error::Release notes for ${RELEASE_TAG} are empty"
exit 1
fi
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Merge existing assets and generate checksums
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
mkdir -p publish
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
if ! gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern "navop-*" \
--pattern "navop_*" \
--dir publish; then
echo "No existing release assets found; publishing only newly built artifacts."
fi
fi
cp -a artifacts/. publish/
cd publish
shopt -s nullglob
release_files=(navop-* navop_*)
if [ "${#release_files[@]}" -eq 0 ]; then
echo "::error::No release artifacts found for checksum generation"
exit 1
fi
sha256sum "${release_files[@]}" > sha256sums.txt
cat sha256sums.txt
- name: Write release metadata
env:
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
RELEASE_PLATFORM: ${{ needs.prepare.outputs.platform }}
run: |
node <<'NODE'
const fs = require("fs");
const releaseTag = process.env.RELEASE_TAG;
const version = releaseTag.replace(/^v/, "");
const platform = process.env.RELEASE_PLATFORM;
fs.mkdirSync("release-metadata", { recursive: true });
fs.writeFileSync(
"release-metadata/release-metadata.json",
`${JSON.stringify({ release_tag: releaseTag, version, platform }, null, 2)}\n`,
);
NODE
- name: Upload release metadata
uses: actions/upload-artifact@v4
with:
name: release-metadata
path: release-metadata/release-metadata.json
- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "Navop $RELEASE_TAG" \
--notes-file release-notes.md
else
gh release create "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "Navop $RELEASE_TAG" \
--notes-file release-notes.md
fi
shopt -s nullglob
new_files=(artifacts/navop-* artifacts/navop_*)
if [ "${#new_files[@]}" -eq 0 ]; then
echo "::error::No newly built release assets found"
exit 1
fi
gh release upload "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--clobber \
"${new_files[@]}" \
publish/sha256sums.txt
# Dispatch R2 explicitly after the Release assets are present.
# Relying only on workflow_run is not reliable when this workflow
# itself is dispatched by release-trigger.yml.
gh workflow run upload-r2.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f tag="$RELEASE_TAG"
gh workflow run sync-cnb-release-assets.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f tag="$RELEASE_TAG"