Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04, windows-2025]
os: [ubuntu-22.04, ubuntu-24.04, windows-2025, macos-15]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
Expand Down
110 changes: 87 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ name: Release
on:
push:
tags: ["v*"]
branches: [main]

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

# Tag v* publishes Linux .deb + AppImage and Windows portable .exe + NSIS.
# Third-party VPN apps stay in-app (ADR 0004); this workflow does not vendor them.
# Build jobs only upload workflow artifacts. The final job publishes all four together.
# Tag v* publishes Linux .deb + AppImage, Windows portable .exe + NSIS, and the
# macOS disk image. A push to main publishes the same release when that version
# does not already have a finished GitHub release (ADR 0123). Third-party VPN
# apps stay in-app (ADR 0004); this workflow does not vendor them.
# Build jobs only upload workflow artifacts. The final job publishes them together.
# Native runners follow the official Tauri GitHub pipeline: Linux WebKitGTK 4.1 on
# Ubuntu 22.04 and 24.04, Windows MSVC + WebView2 on windows-2025. The 22.04
# packages are extra release assets (`*_ubuntu2204_*`) because that glibc is
Expand All @@ -25,7 +28,48 @@ concurrency:
cancel-in-progress: false

jobs:
gate:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
publish: ${{ steps.meta.outputs.publish }}
release_tag: ${{ steps.meta.outputs.release_tag }}
steps:
- uses: actions/checkout@v4
- name: Decide whether this version still needs a release
id: meta
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
version="$(tr -d '[:space:]' < version)"
tag="v${version}"
echo "release_tag=${tag}" >> "${GITHUB_OUTPUT}"
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "${GITHUB_REF_NAME}" != "${tag}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match version file ${tag}"
exit 1
fi
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
echo "publish=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
if view="$(gh release view "${tag}" --json isDraft 2>/dev/null)"; then
draft="$(printf '%s' "${view}" | jq -r '.isDraft')"
if [ "${draft}" = "true" ]; then
echo "publish=true" >> "${GITHUB_OUTPUT}"
else
echo "Release ${tag} is already published"
echo "publish=false" >> "${GITHUB_OUTPUT}"
fi
else
echo "publish=true" >> "${GITHUB_OUTPUT}"
fi

verify:
needs: gate
if: needs.gate.result == 'success' && needs.gate.outputs.publish == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
Expand All @@ -44,14 +88,6 @@ jobs:
with:
workspaces: ". -> target"
prefix-key: ubuntu-24.04
- name: Verify tag matches the version file
shell: bash
run: |
expected="v$(tr -d '[:space:]' < version)"
if [ "${GITHUB_REF_NAME}" != "${expected}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match version file ${expected}"
exit 1
fi
- name: Install Linux desktop dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils && pkg-config --exists webkit2gtk-4.1
- run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -81,6 +117,8 @@ jobs:
args: --bundles deb,appimage
- platform: windows-2025
args: --bundles nsis
- platform: macos-15
args: --bundles dmg
runs-on: ${{ matrix.platform }}
timeout-minutes: 90
steps:
Expand Down Expand Up @@ -138,6 +176,7 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: node scripts/prepare-tauri-signing.mjs --require --github-env --verify-sign
- name: Stage privileged helper
if: runner.os != 'macOS'
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
Expand All @@ -146,8 +185,13 @@ jobs:
./scripts/stage-helper.sh
fi
- uses: tauri-apps/tauri-action@v1
if: runner.os != 'macOS'
with:
args: ${{ matrix.args }}
- name: Build the macOS disk image
if: runner.os == 'macOS'
shell: bash
run: ./build.sh ci-macos
- name: Mark Ubuntu 22.04 packages
if: runner.os == 'Linux'
shell: bash
Expand Down Expand Up @@ -212,9 +256,17 @@ jobs:
target/release/bundle/nsis/*-setup.exe.sig
if-no-files-found: error
retention-days: 7
- name: Upload macOS release artifact
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: release-macos
path: artifacts/macos/*.dmg
if-no-files-found: error
retention-days: 7

publish:
needs: build
needs: [gate, build]
runs-on: ubuntu-24.04
permissions:
contents: write
Expand All @@ -230,19 +282,22 @@ jobs:
merge-multiple: true
- name: Generate and validate latest.json
shell: bash
env:
RELEASE_TAG: ${{ needs.gate.outputs.release_tag }}
run: |
set -euo pipefail
node scripts/generate-latest-json.mjs \
--assets release-assets \
--tag "${GITHUB_REF_NAME}" \
--tag "${RELEASE_TAG}" \
--repo "${{ github.repository }}" \
--output release-assets/latest.json \
--notes "BiFlow ${GITHUB_REF_NAME#v}"
--notes "BiFlow ${RELEASE_TAG#v}"
- name: Validate and publish the complete release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.gate.outputs.release_tag }}
run: |
set -euo pipefail
mapfile -t debs < <(find release-assets -type f -name '*.deb' ! -name '*ubuntu2204*' -print | sort)
Expand All @@ -257,23 +312,32 @@ jobs:
mapfile -t installers < <(find release-assets -type f -name '*-setup.exe' -print | sort)
mapfile -t installer_sigs < <(find release-assets -type f -name '*-setup.exe.sig' -print | sort)
mapfile -t manifests < <(find release-assets -type f -name 'latest.json' -print | sort)
mapfile -t dmgs < <(find release-assets -type f -name '*.dmg' -print | sort)

if (( ${#debs[@]} != 1 || ${#deb_sigs[@]} != 1 || ${#appimages[@]} != 1 || ${#appimage_sigs[@]} != 1 || ${#ubuntu_debs[@]} != 1 || ${#ubuntu_deb_sigs[@]} != 1 || ${#ubuntu_appimages[@]} != 1 || ${#ubuntu_appimage_sigs[@]} != 1 || ${#portable[@]} != 1 || ${#installers[@]} != 1 || ${#installer_sigs[@]} != 1 || ${#manifests[@]} != 1 )); then
echo "Expected Ubuntu 24.04 and Ubuntu 22.04 deb/AppImage pairs with signatures, one portable exe, one NSIS installer and signature, and latest.json"
if (( ${#debs[@]} != 1 || ${#deb_sigs[@]} != 1 || ${#appimages[@]} != 1 || ${#appimage_sigs[@]} != 1 || ${#ubuntu_debs[@]} != 1 || ${#ubuntu_deb_sigs[@]} != 1 || ${#ubuntu_appimages[@]} != 1 || ${#ubuntu_appimage_sigs[@]} != 1 || ${#portable[@]} != 1 || ${#installers[@]} != 1 || ${#installer_sigs[@]} != 1 || ${#manifests[@]} != 1 || ${#dmgs[@]} != 1 )); then
echo "Expected Ubuntu 24.04 and Ubuntu 22.04 deb/AppImage pairs with signatures, one portable exe, one NSIS installer and signature, one macOS disk image, and latest.json"
find release-assets -type f -print
exit 1
fi

assets=("${debs[@]}" "${deb_sigs[@]}" "${appimages[@]}" "${appimage_sigs[@]}" "${ubuntu_debs[@]}" "${ubuntu_deb_sigs[@]}" "${ubuntu_appimages[@]}" "${ubuntu_appimage_sigs[@]}" "${portable[@]}" "${installers[@]}" "${installer_sigs[@]}" "${manifests[@]}")
if ! gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
gh release create "${GITHUB_REF_NAME}" \
tag="${RELEASE_TAG}"
if ! git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "${tag}"
git push origin "refs/tags/${tag}"
fi

assets=("${debs[@]}" "${deb_sigs[@]}" "${appimages[@]}" "${appimage_sigs[@]}" "${ubuntu_debs[@]}" "${ubuntu_deb_sigs[@]}" "${ubuntu_appimages[@]}" "${ubuntu_appimage_sigs[@]}" "${portable[@]}" "${installers[@]}" "${installer_sigs[@]}" "${dmgs[@]}" "${manifests[@]}")
if ! gh release view "${tag}" >/dev/null 2>&1; then
gh release create "${tag}" \
--draft \
--generate-notes \
--title "BiFlow ${GITHUB_REF_NAME}" \
--title "BiFlow ${tag}" \
--verify-tag
fi
gh release upload "${GITHUB_REF_NAME}" "${assets[@]}" --clobber
gh release edit "${GITHUB_REF_NAME}" \
gh release upload "${tag}" "${assets[@]}" --clobber
gh release edit "${tag}" \
--draft=false \
--prerelease=false \
--title "BiFlow ${GITHUB_REF_NAME}"
--title "BiFlow ${tag}"
37 changes: 36 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ If a required command fails or emits a warning from project code, fix it in the

## Lessons

- macOS route parsers used only by native cleanup still compile in Linux and
Windows library builds unless gated with `cfg(any(target_os = "macos", test))`.
Keep their pure route-deletion tests running on every host. Update vulnerable
lockfile dependencies instead of ignoring RustSec advisories. The updater is
used only for signed downloads; keep its unused archive installer disabled so
it cannot reintroduce a second, vulnerable ZIP dependency (ADR 0124).

- Copy native display preferences out of their mutex before `TrayIcon::set_title`.
That operation can synchronously wait for the main thread; keeping the settings
lock in an argument temporary can block a concurrent settings/shutdown action
Expand Down Expand Up @@ -555,7 +562,7 @@ If a required command fails or emits a warning from project code, fix it in the
- React Fast Refresh warns when a component module also exports a plain helper. Put shared helpers such as country-flag conversion in a separate module so `eslint --max-warnings 0` stays clean.
- `cargo deny check` uses an explicit license allow list. Add `MPL-2.0` (cssparser via Tauri/wry), `Apache-2.0 WITH LLVM-exception`, `CDLA-Permissive-2.0`, and `BSL-1.0` (`clipboard-win` via `tauri-plugin-clipboard-manager`) when the lockfile needs them; do not blanket-allow. Workspace `path` crates look like `*` wildcards unless `allow-wildcard-paths = true` and those crates are unpublished (`publish = false`); cargo-deny 0.20 does not apply the path exception to public crates. Transitive unmaintained crates (GTK3 via Tauri, unic via urlpattern, proc-macro-error via glib) fail unless `unmaintained = "workspace"`.
- Windows Clippy `unnecessary_wraps` fires on `#[cfg(not(unix))]` permission stubs that always `Ok(())`. Keep `Result` on the Unix chmod path and a `()` no-op off Unix, with `cfg` at the call site. Helper `main` must `cfg` Unix-only imports such as `tracing_subscriber::EnvFilter`; Windows Clippy treats them as unused.
- `github.ref_protected` is false for typical `v*` tags, so a release job with that `if` never runs. Trigger `.github/workflows/release.yml` on `push: tags: ["v*"]` only, verify before building, upload workflow artifacts, and publish only after every platform succeeds.
- `github.ref_protected` is false for typical `v*` tags, so a release job with that `if` never runs. Trigger `.github/workflows/release.yml` on `push: tags: ["v*"]` and on `main` when that version has no finished release. Verify before building, upload workflow artifacts, and publish only after every platform succeeds. A tag created with the workflow token does not start a second workflow, so the publish job must create the missing `v<version>` tag itself and upload the macOS disk image in that same job (ADR 0123).
- Ubuntu 24.04 renamed AppImage's FUSE 2 runtime package to `libfuse2t64`. Use that exact package in the Ubuntu 24.04 workflow and let `build.sh` select `libfuse2t64` or `libfuse2` from the local apt catalog.
- A palette-only UI request changes the existing RGB tokens in `apps/desktop/src/index.css`. Do not add a second theme runtime, shared component CSS system, or TSX behavior/layout changes unless the request explicitly includes them.
- Vitest's jsdom transform can expose a non-`file:` `import.meta.url`. Tests that inspect frontend source files should resolve them from the package test working directory (for example, `path.join(process.cwd(), "src/index.css")`).
Expand Down Expand Up @@ -719,3 +726,31 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the
traffic. An opaque layered fill also hides Explorer's taskbar tint: retain
layered composition but color-key only the background, and test composed
background continuity and rightmost placement (ADR 0117).

- Quit used to leave the helper-owned Mihomo TUN up while the desktop-owned
local proxy died with the process, and macOS DNS stayed on `127.0.0.1`.
Treat `localhost` / `127.0.0.1` / `::1` as one loopback proxy, clear every
configured local-proxy port, and still delete the TUN when that clear
fails. Stop Mihomo with `SIGTERM` before `SIGKILL` so it can remove its
own routes, flush the OS DNS cache, and release the capture if the desktop
has been gone for 90 seconds (ADR 0120). An update exit stays on Pause.
- A successful macOS DNS restore can still leave every service on
`127.0.0.1` when `system-dns.toml` captured the tunnel resolver, or when
`networksetup` returns success while `scutil --dns` does not change. Names
then fail and a raw IP can still connect. Never store or write loopback as
the original DNS. If the live resolver is still loopback, set the private
default gateway, and delete `netstat` routes that still name the TUN. Kill
a scoped Mihomo orphan on stop when the child handle is already empty
(ADR 0121).
- macOS `NET_RT_IFLIST2` `if_data64.ifi_ibytes` does not match `netstat -ib`
on current Darwin. Menu bar speeds must read `IFMIB_IFDATA` /
`IFDATA_GENERAL`. Wi-Fi reports `IFT_ETHER`, so classify with
`SIOCGIFMEDIA` and require `IFM_ACTIVE`. Exclude `utun`, `awdl`, `bridge`,
`anpi`, and the other virtual prefixes even when their flags look up.
`TrayIcon::set_title` is one proportional line and `None` does not clear
it, so the menu bar item changes width as the digits change. Draw upload
above download with the arrow on the right, in an 11-column 9-point
monospaced attributed title on the `TaoTrayTarget` status button. Hide the
status icon while that title is shown and restore the retained image when
it is cleared. Widen both lines together when a fixed unit is longer
(ADR 0122).
Loading
Loading