Fix UAC diagnostics parity, gk alias, flutter version, and versioned CI channels

- GUI launch_as_admin now matches the CLI's UAC diagnosis: detects
  ERROR_CANCELLED (1223, user declined the prompt) instead of silently
  treating it as success, and reports GetLastError()+exe path for any other
  ShellExecuteW failure, replacing the old single opaque "denied or missing"
  message that made GUI/TUI failures impossible to tell apart.
- generate-key subcommand renamed to `gk` (kept `generate-key` as an alias).
- Fixed a real short-flag collision: GenerateKey's --count used short='c',
  which collides with the global --config short (propagated into every
  subcommand); clap validates the whole command tree on first parse(), so
  this could break parsing for the entire CLI, not just generate-key/gk.
  --count is now short='n'.
- ostp-flutter/pubspec.yaml version was stuck at 0.2.97+12; bumped to 0.4.1+13.
- release.yml: added a resolve-channel job that computes one release tag per
  run instead of repeating the logic in five upload steps. Rolling channel
  pushes now carry the actual Cargo.toml version instead of a bare channel
  name: `{version}-nightly` for the nightly branch, `{version}-beta` for
  pre-release. workflow_dispatch gained a `channel` input restricted to
  nightly/beta only — a manual run can never accidentally publish a "stable"
  release; that still requires an explicit vX.Y.Z tag push.
This commit is contained in:
ospab 2026-07-08 02:52:23 +03:00
parent b2ee9eb010
commit c330a0abe3
4 changed files with 124 additions and 55 deletions

View File

@ -10,6 +10,19 @@ on:
- nightly
- pre-release
workflow_dispatch:
inputs:
channel:
description: >-
Manually build+release just this rolling channel. Stable releases
are NEVER picked here on purpose — cut those only via a real
"vX.Y.Z" tag push, so a manual dispatch can't accidentally publish
a "stable" release.
type: choice
required: true
default: nightly
options:
- nightly
- beta
permissions:
contents: write
@ -21,6 +34,55 @@ env:
RUST_BACKTRACE: short
jobs:
# Computes ONE channel + release tag for this whole run, so every build
# job (native matrix + all 3 GUI platforms + Android) uploads to the exact
# same release under the exact same tag, instead of repeating this logic
# (and risking it drifting out of sync) in five separate places.
#
# Tag shape:
# - real "vX.Y.Z" / "vX.Y.Z-beta.N" tag push -> tag used as-is (stable promotion)
# - push to `nightly` -> "{version}-nightly" (rolling, same tag every push)
# - push to `pre-release` -> "{version}-beta" (rolling, same tag every push)
# - workflow_dispatch -> forced by the `channel` input (nightly|beta only)
resolve-channel:
name: Resolve release channel
runs-on: ubuntu-latest
outputs:
channel: ${{ steps.resolve.outputs.channel }}
tag_name: ${{ steps.resolve.outputs.tag_name }}
prerelease: ${{ steps.resolve.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
- name: Resolve channel, version, and release tag
id: resolve
shell: bash
run: |
set -euo pipefail
BASE_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"([^"]+)"/\1/')
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
CHANNEL="stable"
TAG="${{ github.ref_name }}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
CHANNEL="${{ github.event.inputs.channel }}"
elif [ "${{ github.ref_name }}" = "nightly" ]; then
CHANNEL="nightly"
elif [ "${{ github.ref_name }}" = "pre-release" ]; then
CHANNEL="beta"
else
CHANNEL="nightly"
fi
if [ "$CHANNEL" != "stable" ]; then
TAG="${BASE_VERSION}-${CHANNEL}"
fi
echo "Resolved channel=$CHANNEL tag=$TAG (base version $BASE_VERSION)"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG" >> "$GITHUB_OUTPUT"
echo "prerelease=$([ "$CHANNEL" = "stable" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
check-and-test:
name: Check & Test
runs-on: ubuntu-latest
@ -58,7 +120,7 @@ jobs:
publish-release-matrix:
name: Release for ${{ matrix.target }}
needs: check-and-test
needs: [check-and-test, resolve-channel]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@ -244,22 +306,19 @@ jobs:
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
# release; branch pushes (nightly/pre-release) roll a release named
# after the branch itself — no name remapping needed since
# github.ref_name is already the tag OR the branch name as-is.
tag_name: ${{ github.ref_name }}
# Any branch push is a rolling prerelease; for real version tags,
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
# semver tag (v0.4.1) is a stable release.
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
# Computed once in resolve-channel so every platform/job in this run
# lands on the exact same tag: "{version}-nightly" / "{version}-beta"
# for rolling channel pushes, or the pushed "vX.Y.Z" tag as-is for a
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: ${{ matrix.release_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-windows-gui:
name: Build Windows GUI (Tauri) - ${{ matrix.arch }}
needs: check-and-test
needs: [check-and-test, resolve-channel]
runs-on: windows-latest
strategy:
matrix:
@ -326,22 +385,19 @@ jobs:
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
# release; branch pushes (nightly/pre-release) roll a release named
# after the branch itself — no name remapping needed since
# github.ref_name is already the tag OR the branch name as-is.
tag_name: ${{ github.ref_name }}
# Any branch push is a rolling prerelease; for real version tags,
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
# semver tag (v0.4.1) is a stable release.
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
# Computed once in resolve-channel so every platform/job in this run
# lands on the exact same tag: "{version}-nightly" / "{version}-beta"
# for rolling channel pushes, or the pushed "vX.Y.Z" tag as-is for a
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: ostp-windows-gui-${{ matrix.arch }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux-gui:
name: Build Linux GUI (Tauri) - ${{ matrix.arch }}
needs: check-and-test
needs: [check-and-test, resolve-channel]
runs-on: ubuntu-latest
strategy:
matrix:
@ -394,22 +450,19 @@ jobs:
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
# release; branch pushes (nightly/pre-release) roll a release named
# after the branch itself — no name remapping needed since
# github.ref_name is already the tag OR the branch name as-is.
tag_name: ${{ github.ref_name }}
# Any branch push is a rolling prerelease; for real version tags,
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
# semver tag (v0.4.1) is a stable release.
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
# Computed once in resolve-channel so every platform/job in this run
# lands on the exact same tag: "{version}-nightly" / "{version}-beta"
# for rolling channel pushes, or the pushed "vX.Y.Z" tag as-is for a
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: ostp-linux-gui-${{ matrix.arch }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos-gui:
name: Build macOS GUI (Tauri) - ${{ matrix.arch }}
needs: check-and-test
needs: [check-and-test, resolve-channel]
runs-on: macos-latest
strategy:
matrix:
@ -459,22 +512,19 @@ jobs:
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
# release; branch pushes (nightly/pre-release) roll a release named
# after the branch itself — no name remapping needed since
# github.ref_name is already the tag OR the branch name as-is.
tag_name: ${{ github.ref_name }}
# Any branch push is a rolling prerelease; for real version tags,
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
# semver tag (v0.4.1) is a stable release.
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
# Computed once in resolve-channel so every platform/job in this run
# lands on the exact same tag: "{version}-nightly" / "{version}-beta"
# for rolling channel pushes, or the pushed "vX.Y.Z" tag as-is for a
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: ostp-macos-gui-${{ matrix.arch }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-android:
name: Build Android Client (Flutter) - ${{ matrix.arch }}
needs: check-and-test
needs: [check-and-test, resolve-channel]
runs-on: ubuntu-latest
strategy:
matrix:
@ -535,15 +585,12 @@ jobs:
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
# release; branch pushes (nightly/pre-release) roll a release named
# after the branch itself — no name remapping needed since
# github.ref_name is already the tag OR the branch name as-is.
tag_name: ${{ github.ref_name }}
# Any branch push is a rolling prerelease; for real version tags,
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
# semver tag (v0.4.1) is a stable release.
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
# Computed once in resolve-channel so every platform/job in this run
# lands on the exact same tag: "{version}-nightly" / "{version}-beta"
# for rolling channel pushes, or the pushed "vX.Y.Z" tag as-is for a
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: ostp-flutter/ostp-android-${{ matrix.arch }}.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.2.97+12
version: 0.4.1+13
environment:
sdk: ^3.11.4

View File

@ -762,14 +762,35 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::
let params_str = format!("--port {} --token-file \"{}\"", port, token_file.display());
let params_wstr: Vec<u16> = OsStr::new(&params_str).encode_wide().chain(Some(0)).collect();
#[link(name = "shell32")] extern "system" { fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize; }
#[link(name = "kernel32")] extern "system" { fn GetLastError() -> u32; }
// Use the GUI executable's directory as the working directory so dependencies are found
let cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
let dir_wstr: Vec<u16> = cwd_path.parent().unwrap_or(std::path::Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect();
let ret = unsafe { ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), params_wstr.as_ptr(), dir_wstr.as_ptr(), 0) };
if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); }
// ShellExecuteW's return is a pseudo-HINSTANCE: > 32 means the call itself
// "succeeded" — but that range INCLUDES ERROR_CANCELLED (1223), which is
// exactly what Windows returns when the user clicks "No" on the UAC prompt.
// The old `ret <= 32` check alone treated a user-denied prompt as success,
// silently starting nothing and reporting a single opaque "denied or
// missing" message that could not distinguish "no prompt ever shown"
// (missing exe, ret<=32) from "prompt shown and declined" (ret==1223) from
// any other Win32 failure — exactly the ambiguity blocking diagnosis here.
if ret == 1223 {
anyhow::bail!("UAC elevation was denied. TUN mode requires administrator privileges.");
}
if ret <= 32 {
let win_err = unsafe { GetLastError() };
anyhow::bail!(
"Failed to request UAC elevation for the TUN helper (ShellExecuteW ret={}, \
GetLastError={}, path={}). If this keeps happening with no prompt ever appearing, \
an unsigned binary can be silently blocked by SmartScreen/antivirus during \
elevation try running ostp-gui.exe as Administrator manually.",
ret, win_err, exe.display()
);
}
Ok(())
}

View File

@ -30,6 +30,7 @@ enum Commands {
mode: String,
},
/// Generate a new secure access key
#[command(name = "gk", alias = "generate-key")]
GenerateKey {
/// Format for generated key (hex, base64)
#[arg(long, default_value = "hex")]