769 lines
39 KiB
YAML
769 lines
39 KiB
YAML
name: Release Tauri (cross-platform)
|
||
# meta: ensure Actions indexes this workflow on forks (no behavior change).
|
||
# 触发条件:
|
||
# - 推 v*.*.*-tauri 形式的 tag(与老 Swift 版的 vX.Y.Z 区分开,不冲突)
|
||
# - 手动 dispatch(用于测试构建,不发版)
|
||
#
|
||
# 输出:
|
||
# macOS arm64/x64 .dmg + Windows x64 .msi/.exe + Linux x64 .deb/.rpm/.AppImage,自动作为 GitHub Release 资产上传。
|
||
#
|
||
# macOS 分发:
|
||
# - 配好 APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_ID /
|
||
# APPLE_PASSWORD / APPLE_TEAM_ID 后,Tauri 会做 Developer ID 签名和公证。
|
||
# 用户从浏览器下载后不需要手工 xattr。
|
||
# - 未配置 Apple secrets 时自动回退 ad-hoc 签名,GitHub Actions 会打印 warning。
|
||
# - Windows 没签名(无证书),Win 11 SmartScreen 会警告 "未识别的发布者",用户点"仍要运行"。
|
||
# - 任意一个 platform 失败不影响另一个继续构建(fail-fast: false)。
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*-tauri'
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
build:
|
||
permissions:
|
||
contents: write
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- platform: macos-latest
|
||
rust-target: aarch64-apple-darwin
|
||
updater-target: darwin
|
||
updater-arch: aarch64
|
||
- platform: macos-15-intel
|
||
rust-target: x86_64-apple-darwin
|
||
updater-target: darwin
|
||
updater-arch: x86_64
|
||
- platform: windows-latest
|
||
rust-target: x86_64-pc-windows-msvc
|
||
updater-target: windows
|
||
updater-arch: x86_64
|
||
- platform: ubuntu-22.04
|
||
rust-target: x86_64-unknown-linux-gnu
|
||
updater-target: linux
|
||
updater-arch: x86_64
|
||
runs-on: ${{ matrix.platform }}
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
# 渠道由 tag 后缀决定:
|
||
# v<v>-beta-tauri / v<v>-Beta.N-tauri
|
||
# → beta 渠道(GitHub Release 标 prerelease,manifest 文件名带 -beta 后缀,
|
||
# 正式版用户的 endpoint 拿不到)
|
||
# v<v>-tauri → stable 渠道(正式版,文件名沿用旧约定,向后兼容)
|
||
# workflow_dispatch / 非 tag 触发时 github.ref_name 不是 tag 字符串,
|
||
# endsWith 返回 false,回退为 stable,不改变现有 dispatch 行为。
|
||
OPENLESS_RELEASE_CHANNEL: ${{ (endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.')) && 'beta' || 'stable' }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
# vendor/qwen-asr 是 macOS 上 build.rs 必须的 git submodule(cc-rs 编译
|
||
# Open-Less/qwen-asr fork 的 C 源),不拉就会在 mac 端 cargo build 阶段挂掉。
|
||
submodules: recursive
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: npm
|
||
cache-dependency-path: 'openless-all/app/package-lock.json'
|
||
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: ${{ matrix.rust-target }}
|
||
|
||
- name: Cache Cargo
|
||
uses: swatinem/rust-cache@v2
|
||
with:
|
||
workspaces: 'openless-all/app/src-tauri -> target'
|
||
|
||
- name: Install Linux bundle deps
|
||
if: matrix.platform == 'ubuntu-22.04'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y \
|
||
build-essential \
|
||
curl \
|
||
file \
|
||
libasound2-dev \
|
||
libayatana-appindicator3-dev \
|
||
libfuse2 \
|
||
librsvg2-dev \
|
||
libssl-dev \
|
||
libwebkit2gtk-4.1-dev \
|
||
libxdo-dev \
|
||
patchelf \
|
||
rpm \
|
||
wget
|
||
|
||
- name: Install npm deps
|
||
working-directory: 'openless-all/app'
|
||
run: npm ci
|
||
|
||
- name: Check updater signing availability
|
||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
shell: bash
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
run: |
|
||
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||
echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for signed auto-update artifacts."
|
||
exit 1
|
||
fi
|
||
|
||
- name: Check Apple signing availability
|
||
if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
shell: bash
|
||
env:
|
||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||
run: |
|
||
missing=()
|
||
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
|
||
if [ -z "${!name:-}" ]; then
|
||
missing+=("$name")
|
||
fi
|
||
done
|
||
if [ "${#missing[@]}" -gt 0 ]; then
|
||
echo "::warning::macOS release will use ad-hoc signing because Apple signing/notarization secrets are missing: ${missing[*]}"
|
||
fi
|
||
|
||
- name: Import Apple Developer ID certificate
|
||
if: startsWith(matrix.platform, 'macos')
|
||
shell: bash
|
||
env:
|
||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||
run: |
|
||
if [ -z "${APPLE_CERTIFICATE:-}" ] || [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then
|
||
echo "No Apple certificate secrets configured; macOS build will use ad-hoc signing."
|
||
exit 0
|
||
fi
|
||
|
||
KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-$(openssl rand -base64 32)}"
|
||
CERT_PATH="$RUNNER_TEMP/openless-certificate.p12"
|
||
KEYCHAIN_PATH="$RUNNER_TEMP/openless-build.keychain-db"
|
||
|
||
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
|
||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||
security default-keychain -s "$KEYCHAIN_PATH"
|
||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||
security set-keychain-settings -t 3600 -u "$KEYCHAIN_PATH"
|
||
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||
|
||
DEVELOPER_ID_INFO="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep 'Developer ID Application' | head -n 1)"
|
||
if [ -n "$DEVELOPER_ID_INFO" ]; then
|
||
CERT_INFO="$DEVELOPER_ID_INFO"
|
||
else
|
||
CERT_INFO="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -E 'Apple Distribution|Apple Development' | head -n 1)"
|
||
fi
|
||
if [ -z "$CERT_INFO" ]; then
|
||
echo "Apple certificate imported, but no usable code-signing identity was found."
|
||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||
exit 1
|
||
fi
|
||
|
||
CERT_ID="$(echo "$CERT_INFO" | awk -F'"' '{print $2}')"
|
||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV"
|
||
echo "Imported Apple signing identity: $CERT_ID"
|
||
|
||
- name: Configure Apple notarization
|
||
if: startsWith(matrix.platform, 'macos')
|
||
shell: bash
|
||
env:
|
||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||
APPLE_PROVIDER_SHORT_NAME: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }}
|
||
run: |
|
||
for name in APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID APPLE_PROVIDER_SHORT_NAME; do
|
||
value="${!name:-}"
|
||
if [ -n "$value" ]; then
|
||
echo "$name=$value" >> "$GITHUB_ENV"
|
||
fi
|
||
done
|
||
|
||
# ── macOS:用我们自己的 build-mac.sh,统一处理签名、公证和 artifact 清理 ──
|
||
- name: Build (macOS)
|
||
if: startsWith(matrix.platform, 'macos')
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
INSTALL: '0' # CI 不要装到 /Applications,也不要 reset TCC
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: bash scripts/build-mac.sh
|
||
|
||
# ── Windows:先 build OpenLessIme.dll(x64+x86),再跑 tauri bundle。
|
||
# openless-ime.wxs 用 $(env.OPENLESS_IME_DLL_X64) / _X86 拿绝对路径,
|
||
# 跨 candle/light cwd 都能 resolve(Tauri wix bundler cwd 不固定)。
|
||
- name: Build Windows IME native DLLs
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$appRoot = (Resolve-Path .).Path
|
||
foreach ($t in @(
|
||
@{ Platform = 'x64'; Folder = 'x64'; EnvName = 'OPENLESS_IME_DLL_X64' },
|
||
@{ Platform = 'Win32'; Folder = 'x86'; EnvName = 'OPENLESS_IME_DLL_X86' }
|
||
)) {
|
||
$out = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\$($t.Folder)\Release"
|
||
$obj = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\obj\$($t.Folder)\Release"
|
||
./scripts/windows-ime-build.ps1 -Configuration Release -Platform $t.Platform -OutputDirectory $out -IntermediateDirectory $obj
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "OpenLessIme $($t.Platform) build failed with exit $LASTEXITCODE"
|
||
}
|
||
$dll = (Resolve-Path (Join-Path $out 'OpenLessIme.dll')).Path
|
||
if (-not (Test-Path $dll)) {
|
||
throw "OpenLessIme.dll not produced at $dll"
|
||
}
|
||
"$($t.EnvName)=$dll" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||
Write-Host "[ok] built $dll (exported $($t.EnvName))"
|
||
|
||
# bundle.resources 引用的是 src-tauri/openless-ime-payload/{x64,x86}/OpenLessIme.dll
|
||
# (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。
|
||
# 这里用真 dll 覆盖占位,让 NSIS / MSI 都装上真文件;NSIS hook 的 regsvr32
|
||
# 同时会把 64 / 32 位 COM 注册到 HKLM\Software\Classes\CLSID 的
|
||
# KEY_WOW64_64KEY / KEY_WOW64_32KEY 两侧(windows_ime_profile.rs 都会查)。
|
||
$payloadDir = Join-Path $appRoot "src-tauri\openless-ime-payload\$($t.Folder)"
|
||
New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null
|
||
Copy-Item -Force -Path $dll -Destination (Join-Path $payloadDir 'OpenLessIme.dll')
|
||
Write-Host "[ok] copied real $($t.Folder) dll into bundle.resources payload path"
|
||
}
|
||
|
||
# ── Windows tauri build:保持 bash shell,因为 PowerShell 调外部命令
|
||
# 会把 '{"bundle":...}' 的内部双引号吃掉、让 tauri 收到无效 JSON。
|
||
# 用 set +e + GITHUB_ENV 把 exit code 传到下一步给 Repair 判断。
|
||
- name: Build (Windows)
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: bash
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: |
|
||
set +e
|
||
# 拆两轮跑:Tauri 的签名 / updater artifact 阶段是 post-bundle 钩子,
|
||
# 任意 bundler 失败会让 *所有* bundle 的 .sig 跳过。MSI 必踩 ICE80,
|
||
# 所以单一 `tauri build` 永远拿不到 NSIS 的 .exe.sig。
|
||
# Pass 1:NSIS 独立跑——必须成功,产出 *_x64-setup.exe(.sig) 给 updater。
|
||
# Pass 2:MSI 独立跑——允许失败,Repair 步骤兜底 light.exe 重链。
|
||
# Beta 版跳过 MSI:Windows Installer / WiX 无法表示 `1.2.3-Beta.1`
|
||
# 这类非纯数字 prerelease 标识,稳定版仍然照常产出 MSI。
|
||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||
npm run tauri -- build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true}}'
|
||
nsis_exit=$?
|
||
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
|
||
echo "[info] Skipping MSI bundle for beta release channel."
|
||
msi_exit=0
|
||
else
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV"
|
||
npm run tauri -- build --bundles msi --config '{"bundle":{"createUpdaterArtifacts":true}}'
|
||
msi_exit=$?
|
||
fi
|
||
else
|
||
npm run tauri -- build --bundles nsis
|
||
nsis_exit=$?
|
||
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
|
||
echo "[info] Skipping MSI bundle for beta release channel."
|
||
msi_exit=0
|
||
else
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV"
|
||
npm run tauri -- build --bundles msi
|
||
msi_exit=$?
|
||
fi
|
||
fi
|
||
echo "TAURI_BUILD_EXIT=$msi_exit" >> "$GITHUB_ENV"
|
||
# NSIS 是 updater 的硬依赖,挂了就直接 fail step。
|
||
if [ "$nsis_exit" -ne 0 ]; then
|
||
echo "::error::NSIS bundle failed (exit $nsis_exit) — updater artifact unavailable."
|
||
exit 1
|
||
fi
|
||
# MSI 失败不挡——下一步 Repair 用 light.exe 重链(带 -sice:ICE80)。
|
||
exit 0
|
||
|
||
# ── 如果 tauri 在 wix link 阶段失败(candle 出了 wixobj,但 light 因 cwd
|
||
# 解析 wxs Source 找不到 IME DLL),从 appRoot 手动跑 light 兜底重链。
|
||
# 这是本地 windows-package-msvc.ps1::Repair-TauriMsiBundle 的同款手术。
|
||
- name: Repair Windows MSI if Tauri failed at WiX link
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$exitCode = $env:TAURI_BUILD_EXIT
|
||
if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') {
|
||
Write-Host "[ok] Skipping MSI repair for beta release channel."
|
||
return
|
||
}
|
||
$appRoot = (Resolve-Path .).Path
|
||
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
if ($msi) {
|
||
Write-Host "[ok] MSI already produced by tauri: $($msi.FullName); no repair needed."
|
||
return
|
||
}
|
||
if (-not $exitCode -or $exitCode -eq '0') {
|
||
throw "Tauri exited 0 but no MSI found at src-tauri\target\release\bundle\msi\"
|
||
}
|
||
Write-Warning "Tauri MSI failed (exit $exitCode). Attempting manual light.exe relink from app root."
|
||
$wixRoot = Join-Path $appRoot 'src-tauri\target\release\wix\x64'
|
||
$mainObj = Join-Path $wixRoot 'main.wixobj'
|
||
$imeObj = Join-Path $wixRoot 'openless-ime.wixobj'
|
||
$locale = Join-Path $wixRoot 'locale.wxl'
|
||
foreach ($p in @($mainObj, $imeObj, $locale)) {
|
||
if (-not (Test-Path $p)) {
|
||
throw "Required WiX object missing: $p — tauri build aborted before candle ran. Check the Build (Windows) step log."
|
||
}
|
||
}
|
||
$light = Get-ChildItem "$env:LOCALAPPDATA\tauri\WixTools*\light.exe" -ErrorAction SilentlyContinue |
|
||
Sort-Object FullName |
|
||
Select-Object -Last 1 -ExpandProperty FullName
|
||
if (-not $light) { throw "WiX light.exe not found under $env:LOCALAPPDATA\tauri\WixTools*" }
|
||
$version = (Get-Content src-tauri\tauri.conf.json -Raw | ConvertFrom-Json).version
|
||
$bundleDir = Join-Path $appRoot 'src-tauri\target\release\bundle\msi'
|
||
New-Item -ItemType Directory -Force -Path $bundleDir | Out-Null
|
||
$msiPath = Join-Path $bundleDir "OpenLess_${version}_x64_en-US.msi"
|
||
Push-Location $appRoot
|
||
try {
|
||
# -sice:ICE80:x86 IME DLL 与 x64 一同打进 INSTALLDIR\windows-ime\,
|
||
# 这是 32 位组件落在 64 位 Directory 下的合法场景(DLL 路径绝对指向,
|
||
# 不依赖 SysWOW64 重定向)。Tauri 自身没有暴露 light 透传参数,所以
|
||
# 必须在这里抑制,否则 LGHT0204 必失败。
|
||
& $light -nologo -sice:ICE80 -ext WixUIExtension -ext WixUtilExtension -loc $locale -out $msiPath $mainObj $imeObj
|
||
if ($LASTEXITCODE -ne 0) { throw "light.exe relink failed with exit $LASTEXITCODE" }
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
Write-Host "[ok] MSI rebuilt at $msiPath"
|
||
|
||
- name: Verify Windows installers register TSF IME
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$nsis = Get-ChildItem "src-tauri\target\release\bundle\nsis\*.exe" -ErrorAction Stop | Select-Object -First 1
|
||
if (-not $nsis) { throw "NSIS installer not found." }
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $nsis.FullName -InstallerKind nsis
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "NSIS installer smoke failed with exit $LASTEXITCODE"
|
||
}
|
||
if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') {
|
||
Write-Host "[ok] Skipping MSI smoke for beta release channel."
|
||
return
|
||
}
|
||
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction Stop | Select-Object -First 1
|
||
if (-not $msi) { throw "MSI installer not found." }
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $msi.FullName -InstallerKind msi
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "MSI installer smoke failed with exit $LASTEXITCODE"
|
||
}
|
||
|
||
# ── Linux:先编译 fcitx5 插件,再产 deb / rpm / AppImage ──
|
||
- name: Build fcitx5 plugin
|
||
if: matrix.platform == 'ubuntu-22.04'
|
||
shell: bash
|
||
working-directory: 'openless-all/scripts/linux-fcitx5-plugin'
|
||
run: |
|
||
# v1.3.4-3 之后 Ubuntu 22.04 universe 把 fcitx5-dev 这个 meta 包移除了
|
||
# (v1.3.4-4/-5/-6 在此步连挂三次,错误 "Unable to locate package fcitx5-dev")。
|
||
# v1.3.4-6 加的 apt-cache search 诊断显示,22.04 jammy 实际只剩拆分后的
|
||
# libfcitx5{core,utils,config}-dev / fcitx5-modules-dev 等子包,
|
||
# 但 fcitx5-dev 这个 umbrella 包确认已不在源里。
|
||
#
|
||
# CMakeLists.txt 实际只需要:
|
||
# find_package(Fcitx5Core) → libfcitx5core-dev
|
||
# find_package(Fcitx5Utils) → libfcitx5utils-dev
|
||
# find_package(Fcitx5Module) → fcitx5-modules-dev
|
||
# 另外 fcitx5 自己的 CMake config 文件依赖 KDE ECM。
|
||
# 保留 add-apt-repository universe + 本步骤内 apt-get update 作为通用防御。
|
||
sudo add-apt-repository -y universe || true
|
||
sudo apt-get update
|
||
if ! sudo apt-get install -y \
|
||
cmake \
|
||
extra-cmake-modules \
|
||
libfcitx5core-dev \
|
||
libfcitx5utils-dev \
|
||
libfcitx5config-dev \
|
||
fcitx5-modules-dev; then
|
||
echo "::error::fcitx5 dev packages install failed. Diagnostics ↓"
|
||
echo "--- apt-cache search fcitx5 ---"
|
||
apt-cache search fcitx5 || true
|
||
echo "--- /etc/os-release ---"
|
||
cat /etc/os-release || true
|
||
echo "--- /etc/apt/sources.list.d ---"
|
||
ls -la /etc/apt/sources.list.d/ || true
|
||
exit 1
|
||
fi
|
||
mkdir -p build && cd build
|
||
# 显式 CMAKE_INSTALL_PREFIX=/usr:fcitx5 在 Ubuntu 上的运行时插件搜索
|
||
# 路径是 /usr/lib/<arch>/fcitx5/,默认 prefix=/usr/local 会让 .deb 把
|
||
# 插件安到 /usr/local/lib/fcitx5/,fcitx5 永远找不到。
|
||
# 历史上 v1.3.4-3 也是这条 bug 路径,只是 cmake -LA 那行先挂掉,没人意识到。
|
||
cmake .. -DCMAKE_INSTALL_PREFIX=/usr 2>&1 | tee cmake-configure.log
|
||
make
|
||
# v1.3.4-7 暴露:fcitx5 5.0.14 把 FCITX_INSTALL_* 设为普通变量而非 CACHE,
|
||
# `cmake -LA` 抓不到 → grep 退码 1 → set -e -o pipefail 整步退出。
|
||
# 改从 configure 阶段 message(STATUS ...) 的输出抓(CMakeLists.txt 已经打印)。
|
||
FCITX_ADDON_DIR=$(grep "FCITX_INSTALL_ADDONDIR:" cmake-configure.log \
|
||
| tail -1 | sed -E 's/^[^:]*: ([^ ]+).*/\1/')
|
||
FCITX_PKGDATA_DIR=$(grep "FCITX_INSTALL_PKGDATADIR:" cmake-configure.log \
|
||
| tail -1 | sed -E 's/^[^:]*: ([^ ]+).*/\1/')
|
||
if [ -z "$FCITX_ADDON_DIR" ] || [ -z "$FCITX_PKGDATA_DIR" ]; then
|
||
echo "::error::Failed to extract FCITX_INSTALL_* paths from cmake configure log"
|
||
echo "--- cmake-configure.log (last 50 lines) ---"
|
||
tail -50 cmake-configure.log
|
||
exit 1
|
||
fi
|
||
echo "Detected: addon=$FCITX_ADDON_DIR pkgdata=$FCITX_PKGDATA_DIR"
|
||
echo "FCITX_ADDON_DIR=$FCITX_ADDON_DIR" >> "$GITHUB_ENV"
|
||
echo "FCITX_ADDON_CONF_DIR=${FCITX_PKGDATA_DIR}/addon" >> "$GITHUB_ENV"
|
||
# 对 RPM 目标映射路径:Debian multiarch(如 /usr/lib/x86_64-linux-gnu)
|
||
# -> /usr/lib64(RPM 标准)。conf 路径跨发行版一致。
|
||
RPM_ADDON_DIR=$(echo "$FCITX_ADDON_DIR" \
|
||
| sed 's|/usr/lib/[^/]*/fcitx5|/usr/lib64/fcitx5|;s|/usr/lib/x86_64-linux-gnu/fcitx5|/usr/lib64/fcitx5|')
|
||
echo "FCITX_RPM_ADDON_DIR=$RPM_ADDON_DIR" >> "$GITHUB_ENV"
|
||
# 把插件 .so + .conf 复制到 src-tauri/linux-fcitx5-plugin/ 下面,
|
||
# 供 tauri deb/rpm bundler 的 files 配置使用。
|
||
mkdir -p "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin"
|
||
cp libopenless.so "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin/"
|
||
cp openless.conf "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin/"
|
||
|
||
- name: Build (Linux)
|
||
if: matrix.platform == 'ubuntu-22.04'
|
||
shell: bash
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: |
|
||
# deb/rpm:通过 files 映射把插件安装到系统 fcitx5 路径。
|
||
# AppImage:通过 bundle.resources 把 .so 打进包内,运行时由
|
||
# ensure_plugin_installed() 自动安装到 ~/.local/ 下。
|
||
# 插件 .so + .conf 由上一步 Build fcitx5 plugin 生成并复制到
|
||
# src-tauri/linux-fcitx5-plugin/ 下。
|
||
cat > /tmp/tauri-linux-config.json << CONFIG_EOF
|
||
{
|
||
"bundle": {
|
||
"resources": ["linux-fcitx5-plugin/libopenless.so"],
|
||
"linux": {
|
||
"deb": {
|
||
"depends": ["fcitx5", "fcitx5-module-dbus", "libdbus-1-3"],
|
||
"files": {
|
||
"${FCITX_ADDON_DIR}/libopenless.so": "linux-fcitx5-plugin/libopenless.so",
|
||
"${FCITX_ADDON_CONF_DIR}/openless.conf": "linux-fcitx5-plugin/openless.conf"
|
||
}
|
||
},
|
||
"rpm": {
|
||
"depends": ["fcitx5", "fcitx5-module-dbus"],
|
||
"files": {
|
||
"${FCITX_RPM_ADDON_DIR}/libopenless.so": "linux-fcitx5-plugin/libopenless.so",
|
||
"${FCITX_ADDON_CONF_DIR}/openless.conf": "linux-fcitx5-plugin/openless.conf"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
CONFIG_EOF
|
||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||
jq '.bundle.createUpdaterArtifacts = true' /tmp/tauri-linux-config.json > /tmp/tauri-linux-config-signed.json
|
||
CONFIG_FILE=/tmp/tauri-linux-config-signed.json
|
||
else
|
||
CONFIG_FILE=/tmp/tauri-linux-config.json
|
||
fi
|
||
npm run tauri -- build --bundles deb,rpm,appimage --config "$CONFIG_FILE"
|
||
|
||
- name: Disambiguate macOS updater bundle filename
|
||
if: startsWith(matrix.platform, 'macos') && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||
shell: bash
|
||
working-directory: 'openless-all/app/src-tauri/target/release/bundle/macos'
|
||
run: |
|
||
if [ -f OpenLess.app.tar.gz ]; then
|
||
mv OpenLess.app.tar.gz "OpenLess_${{ matrix.updater-arch }}.app.tar.gz"
|
||
fi
|
||
if [ -f OpenLess.app.tar.gz.sig ]; then
|
||
mv OpenLess.app.tar.gz.sig "OpenLess_${{ matrix.updater-arch }}.app.tar.gz.sig"
|
||
fi
|
||
|
||
- name: Write updater manifest
|
||
if: env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||
shell: bash
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
OPENLESS_UPDATE_TARGET: ${{ matrix.updater-target }}
|
||
OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }}
|
||
OPENLESS_UPDATE_REPO: appergb/openless
|
||
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
|
||
# beta 渠道时输出 latest-{tgt}-{arch}-beta.json,stable 沿用旧文件名。
|
||
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
|
||
# Beta 渠道里脚本要把 manifest.url 写成 releases/download/<tag>/...
|
||
# 而不是 releases/latest(后者永远 = Stable,Beta 用户按 url 下载会拉到错文件)。
|
||
# workflow_dispatch 时 github.ref_name 不是 tag——但那种情况下 channel=stable,
|
||
# 脚本不会读这个字段,安全。
|
||
OPENLESS_RELEASE_TAG: ${{ github.ref_name }}
|
||
run: node scripts/write-updater-manifest.mjs
|
||
|
||
# ── 收集产物 ──
|
||
- name: List artifacts (debug)
|
||
shell: bash
|
||
working-directory: 'openless-all/app/src-tauri/target/release/bundle'
|
||
run: ls -la macos/ dmg/ nsis/ msi/ deb/ rpm/ appimage/ 2>/dev/null || true
|
||
|
||
# 防御性步骤:剥掉 macOS 产物上任何残留扩展属性 / quarantine。
|
||
# 理论上 GitHub Actions 输出的 .app/.dmg 不会带 com.apple.quarantine
|
||
# (xattr 也不会通过 actions/upload-artifact 跨机器持久化),但保留这一步
|
||
# 让"云端 artifact 一定干净"成为可验证的承诺。用户下载后再被本地浏览器
|
||
# 加 quarantine 时,按 release notes 的 `xattr -cr` 一行即可消除。
|
||
- name: Strip xattr / quarantine on macOS bundles
|
||
if: startsWith(matrix.platform, 'macos')
|
||
shell: bash
|
||
working-directory: 'openless-all/app/src-tauri/target/release/bundle'
|
||
run: |
|
||
for path in macos/*.app dmg/*.dmg; do
|
||
if [ -e "$path" ]; then
|
||
echo "▶ stripping xattr: $path"
|
||
xattr -cr "$path" || true
|
||
xattr -lr "$path" || true
|
||
fi
|
||
done
|
||
|
||
- name: Upload macOS artifacts
|
||
if: startsWith(matrix.platform, 'macos')
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-macos-${{ matrix.updater-arch }}
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
|
||
if-no-files-found: error
|
||
|
||
- name: Upload macOS updater artifacts
|
||
if: startsWith(matrix.platform, 'macos') && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-macos-${{ matrix.updater-arch }}-updater
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||
openless-all/app/src-tauri/target/release/bundle/latest-darwin-${{ matrix.updater-arch }}*.json
|
||
if-no-files-found: error
|
||
|
||
- name: Upload Windows artifacts
|
||
if: matrix.platform == 'windows-latest'
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-windows-x64
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe
|
||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
|
||
if-no-files-found: error
|
||
|
||
- name: Upload Windows updater artifacts
|
||
if: matrix.platform == 'windows-latest' && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-windows-x64-updater
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
|
||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
|
||
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64*.json
|
||
if-no-files-found: error
|
||
|
||
- name: Upload fcitx5 plugin artifact (standalone)
|
||
if: matrix.platform == 'ubuntu-22.04'
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-fcitx5-plugin-linux-x64
|
||
path: |
|
||
${{ github.workspace }}/openless-all/scripts/linux-fcitx5-plugin/build/libopenless.so
|
||
${{ github.workspace }}/openless-all/scripts/linux-fcitx5-plugin/build/openless.conf
|
||
if-no-files-found: error
|
||
|
||
- name: Upload Linux artifacts
|
||
if: matrix.platform == 'ubuntu-22.04'
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-linux-x64
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/deb/*.deb
|
||
openless-all/app/src-tauri/target/release/bundle/rpm/*.rpm
|
||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage
|
||
if-no-files-found: error
|
||
|
||
- name: Upload Linux updater artifacts
|
||
if: matrix.platform == 'ubuntu-22.04' && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-linux-x64-updater
|
||
path: |
|
||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||
openless-all/app/src-tauri/target/release/bundle/latest-linux-x86_64*.json
|
||
if-no-files-found: error
|
||
|
||
# ── tag 推送时,同步上传到 GitHub Release ──
|
||
# 只有 leader job (darwin/aarch64) 把 release body 写到文件,其余 matrix job
|
||
# 的 body_path 留空,softprops/action-gh-release@v2 在 body 为空时会保留
|
||
# existing release body 不动,避免每个 matrix job 都 append 一遍同样的 prelude
|
||
# 导致 release notes 重复 N 次 (v1.3.4-tauri 出现 4 次的 root cause:
|
||
# 4 个 matrix job × append_body=true × 共享同一份 body)。
|
||
- name: Prepare release body prelude
|
||
if: matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
shell: bash
|
||
run: |
|
||
cat > "$RUNNER_TEMP/release-body.md" << 'EOF'
|
||
### macOS 用户首次安装提示
|
||
|
||
下载 DMG 拖入 `/Applications` 后,**必须**在终端运行:
|
||
|
||
```bash
|
||
xattr -cr /Applications/OpenLess.app
|
||
```
|
||
|
||
否则 Gatekeeper 会提示「OpenLess 已损坏」——这是因为当前 build 用 ad-hoc 签名、没做 Apple 公证。
|
||
|
||
### 渠道说明
|
||
|
||
- 以 `-tauri` 结尾的 tag 是**正式版**(自动推送给所有 in-app 检查更新的用户)。
|
||
- 以 `-beta-tauri` 结尾或包含 `-Beta.N-tauri` 的 tag 是 **Beta 版**(GitHub 标 pre-release,**不**通过 in-app updater 推送给正式版用户;只对在「设置 → 关于 → 加入 Beta 渠道」开关切到 Beta 的用户可见,且需要手动从此页面下载安装)。
|
||
|
||
### 行为变更提示
|
||
|
||
- 流式输入默认开启;不兼容场景会自动回落到一次性插入。可在「设置 → 高级」关闭。
|
||
- 流式输入成功后默认把最终文本同步到剪贴板,方便再次粘贴;可在「设置 → 高级」关闭。
|
||
EOF
|
||
echo "OPENLESS_RELEASE_BODY_PATH=$RUNNER_TEMP/release-body.md" >> "$GITHUB_ENV"
|
||
|
||
- name: Create / update release
|
||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: ${{ github.ref_name }}
|
||
name: 'OpenLess ${{ github.ref_name }}'
|
||
draft: false
|
||
# beta 渠道的 release 必须标 prerelease=true:GitHub UI 会折叠它,
|
||
# 普通用户看不到;同时只上传 latest-*-beta.json,正式版用户的
|
||
# endpoint(latest-*.json)永远不会被覆盖,保证 Beta 不溢出正式版。
|
||
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
|
||
# 非 leader job 的 OPENLESS_RELEASE_BODY_PATH 是空字符串,softprops 在
|
||
# body 为空时走 `body || existing.body` 分支保留既有内容,不会覆盖。
|
||
# leader job 用默认 append_body=false,每次完整覆盖 release body 为
|
||
# "prelude + generated notes",re-run 同一 tag 时也保持 idempotent
|
||
# (append_body=true 会让 re-run 把上轮 body 拼到前面、再次复制)。
|
||
body_path: ${{ env.OPENLESS_RELEASE_BODY_PATH }}
|
||
# generate_release_notes 也只在 leader 跑:避免 4 个 matrix jobs
|
||
# 产生 4 份相同的 What's Changed 段。
|
||
generate_release_notes: ${{ matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' }}
|
||
files: |
|
||
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
|
||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe
|
||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
|
||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
|
||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
|
||
openless-all/app/src-tauri/target/release/bundle/deb/*.deb
|
||
openless-all/app/src-tauri/target/release/bundle/rpm/*.rpm
|
||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage
|
||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||
openless-all/app/src-tauri/target/release/bundle/latest-*.json
|
||
# fcitx5 插件独立下载:供 AppImage 用户或手动安装
|
||
openless-all/scripts/linux-fcitx5-plugin/build/libopenless.so
|
||
openless-all/scripts/linux-fcitx5-plugin/build/openless.conf
|
||
|
||
# ── 正式版发布后,自动更新 Homebrew cask ──
|
||
# 为什么放进这条流水线,而不是单独的 `release: published` 工作流:softprops 用默认
|
||
# GITHUB_TOKEN 创建的 Release 不会触发 `release` 事件的其它工作流(GitHub 防递归),
|
||
# 所以独立的 update-cask 工作流永远不会被自动触发(历史上只在一次手动场景跑过且失败)。
|
||
# 用 needs: build 串在构建之后、同一条流水线里,才能保证每次正式发版都自动更新 cask。
|
||
#
|
||
# 仅正式版:v*-tauri 且非 -beta-tauri。beta 不碰 Homebrew,避免把预发布版推给
|
||
# `brew install --cask openless` 的用户。cask 文件在默认分支(beta)上,提交回该分支。
|
||
update-homebrew-cask:
|
||
name: Update Homebrew cask (stable only)
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
if: >-
|
||
startsWith(github.ref, 'refs/tags/v')
|
||
&& endsWith(github.ref, '-tauri')
|
||
&& !endsWith(github.ref, '-beta-tauri')
|
||
&& !contains(github.ref_name, '-Beta.')
|
||
steps:
|
||
- name: Checkout default branch (cask 住在这里)
|
||
uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.repository.default_branch }}
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Resolve version + DMG sha256 from the release
|
||
id: meta
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
TAG: ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
version="${TAG#v}"
|
||
version="${version%-tauri}"
|
||
|
||
# 直接下载 DMG 自己算 sha256,不依赖 GitHub 资产 digest 字段的时序/可用性。
|
||
mkdir -p "$RUNNER_TEMP/dmg"
|
||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||
--dir "$RUNNER_TEMP/dmg" \
|
||
--pattern '*aarch64.dmg' --pattern '*_x64.dmg'
|
||
|
||
arm_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*aarch64.dmg | cut -d' ' -f1)"
|
||
intel_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*_x64.dmg | cut -d' ' -f1)"
|
||
|
||
{
|
||
echo "version=$version"
|
||
echo "arm_sha=$arm_sha"
|
||
echo "intel_sha=$intel_sha"
|
||
} >> "$GITHUB_OUTPUT"
|
||
echo "cask → version=$version arm=$arm_sha intel=$intel_sha"
|
||
|
||
- name: Update Casks/openless.rb
|
||
env:
|
||
VERSION: ${{ steps.meta.outputs.version }}
|
||
ARM_SHA: ${{ steps.meta.outputs.arm_sha }}
|
||
INTEL_SHA: ${{ steps.meta.outputs.intel_sha }}
|
||
run: |
|
||
set -euo pipefail
|
||
cask="Casks/openless.rb"
|
||
# ⚠️ cask 第 2 行 `arch arm: "aarch64", intel: "x64"` 也含 `intel: "..."`,
|
||
# 裸 `s/intel: "..."/` 会把 arch 指令一起改成哈希 → Intel 用户 brew 装包 404。
|
||
# 所以 intel 的 sed 用 `^[[:space:]]*intel:` 锚定 sha256 块那一行;arm 的 sed
|
||
# 靠 `sha256 arm: ` 前缀天然只命中 sha256 行。捕获组 \1 保留原缩进。
|
||
sed -i "s/version \"[^\"]*\"/version \"$VERSION\"/" "$cask"
|
||
sed -i "s/sha256 arm: \"[^\"]*\"/sha256 arm: \"$ARM_SHA\"/" "$cask"
|
||
sed -i "s/^\([[:space:]]*\)intel: \"[^\"]*\"/\1intel: \"$INTEL_SHA\"/" "$cask"
|
||
# 防呆:arch 指令必须原样保留,且本次 sha256 必须确实写进去了,否则让 job 失败。
|
||
grep -q 'arch arm: "aarch64", intel: "x64"' "$cask" \
|
||
|| { echo "::error::arch 指令被 sed 误伤"; exit 1; }
|
||
grep -q "\"$ARM_SHA\"" "$cask" && grep -q "\"$INTEL_SHA\"" "$cask" \
|
||
|| { echo "::error::sha256 未正确写入 cask"; exit 1; }
|
||
echo "----- updated $cask -----"
|
||
cat "$cask"
|
||
|
||
- name: Commit & push cask bump
|
||
env:
|
||
VERSION: ${{ steps.meta.outputs.version }}
|
||
run: |
|
||
set -euo pipefail
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
git add Casks/openless.rb
|
||
if git diff --cached --quiet; then
|
||
echo "cask 已是 $VERSION,无需提交"
|
||
exit 0
|
||
fi
|
||
# [skip ci]:cask 文本改动不需要再跑一遍跨平台 ci.yml。
|
||
git commit -m "[cask] openless $VERSION (auto from release) [skip ci]"
|
||
git push
|