Skip to content

fix: make desktop updates transactional and restart the shell #169

fix: make desktop updates transactional and restart the shell

fix: make desktop updates transactional and restart the shell #169

Workflow file for this run

name: FreeOS Desktop Package
# Filename kept as octop-desktop.yml so auto-tag / existing dispatch keep working.
# Publishes FreeOS-desktop-* / FreeOS-portable-*. Default packaging is zero-Node;
# this workflow explicitly sets SHIP_OPENXYOS_RUNTIME=1 for the transitional
# managed-Node + iframe desktop bridge (not the destination architecture).
#
# 正式发版:Auto Tag 在打 v* 后与 Release / Docker Publish 一并 dispatch。
# 产物 upsert 到同一 GitHub Release。手工补包:workflow_dispatch + release_tag
# (不必跑在 v* ref 上)。attach_from_run 只挂已有 artifact、不重打。
on:
push:
tags:
- "v*"
pull_request:
paths:
- "desktop/**"
- "modules/openxyos/**"
- ".github/workflows/octop-desktop.yml"
workflow_dispatch:
inputs:
platforms:
description: "Comma-separated plats, or 'all'"
required: true
default: "all"
type: string
attach_release:
description: "Upload artifacts to a GitHub Release (v* ref, or pass release_tag)"
required: false
default: true
type: boolean
release_tag:
description: "GitHub Release tag to attach to (e.g. v0.0.2). Used when not running on a v* ref."
required: false
default: ""
type: string
attach_from_run:
description: "Existing workflow run ID — attach its FreeOS-* artifacts and skip the 6-platform rebuild"
required: false
default: ""
type: string
permissions:
contents: read
# Builds on the same ref cancel each other (PR retrigger / accidental double
# dispatch). Attach-only runs use a different group so they cannot cancel an
# in-flight six-platform package on develop.
concurrency:
group: >-
green-portable-${{
github.event.inputs.attach_from_run != '' && 'attach' || 'build'
}}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == '' }}
env:
# Prefer GitHub upstream on Actions runners (npmmirror is for CN local builds).
PBS_BASE_URL: https://github.com/astral-sh/python-build-standalone/releases/download/20251209
PBS_TAG: "20251209"
PBS_PY: "3.12.12"
jobs:
frontend:
name: Build dashboard
if: github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: dashboard/package-lock.json
- name: Build frontend → src/octop/dashboard
run: make build-frontend
- uses: actions/upload-artifact@v7
with:
name: dashboard-dist
path: src/octop/dashboard/
if-no-files-found: error
retention-days: 7
# Job-level `if` cannot read `matrix.*` (GitHub 422). Filter here so only
# selected runners start — PRs → darwin-* + windows-*; dispatch honors `platforms`.
select-platforms:
name: Select platforms
if: github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == ''
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
steps:
- id: set
env:
EVENT_NAME: ${{ github.event_name }}
PLATFORMS: ${{ github.event.inputs.platforms || 'all' }}
run: |
set -euo pipefail
python3 <<'PY'
import json
import os
all_plats = [
{"plat": "linux-amd64", "arch": "amd64", "os": "ubuntu-latest"},
{"plat": "linux-arm64", "arch": "arm64", "os": "ubuntu-24.04-arm"},
{"plat": "darwin-arm64", "arch": "arm64", "os": "macos-14"},
{"plat": "darwin-amd64", "arch": "amd64", "os": "macos-15-intel"},
{"plat": "windows-amd64", "arch": "amd64", "os": "windows-latest"},
{"plat": "windows-arm64", "arch": "arm64", "os": "windows-11-arm"},
]
if os.environ["EVENT_NAME"] == "pull_request":
sel = "darwin-arm64,darwin-amd64,windows-amd64,windows-arm64"
else:
sel = os.environ.get("PLATFORMS", "all").lower().replace(" ", "")
known = {row["plat"] for row in all_plats}
if sel == "all":
include = all_plats
else:
wanted = {part for part in sel.split(",") if part}
unknown = wanted - known
if unknown:
raise SystemExit(f"unknown platform(s): {', '.join(sorted(unknown))}")
include = [row for row in all_plats if row["plat"] in wanted]
if not include:
raise SystemExit(f"no platforms selected: {sel}")
matrix = json.dumps({"include": include}, separators=(",", ":"))
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"matrix={matrix}\n")
print(matrix)
PY
package:
name: "${{ matrix.plat }}"
needs: [frontend, select-platforms]
runs-on: ${{ matrix.os }}
# Matrix runners are native for each plat; pin host detection so x64 Git Bash
# on windows-11-arm does not mis-classify the job as windows-amd64 cross-build.
env:
GREEN_HOST_PLAT: ${{ matrix.plat }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.select-platforms.outputs.matrix) }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v5
- name: Read package version
run: |
set -euo pipefail
VER=$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' pyproject.toml | head -1)
requested_tag="${{ github.event.inputs.release_tag }}"
if [[ "${GITHUB_REF}" == refs/tags/v* && "${GITHUB_REF_NAME#v}" != "$VER" ]]; then
echo "Refusing desktop release: tag ${GITHUB_REF_NAME} does not match pyproject.toml $VER" >&2
exit 1
fi
if [[ -n "$requested_tag" && "${requested_tag#v}" != "$VER" ]]; then
echo "Refusing desktop release: requested tag $requested_tag does not match pyproject.toml $VER" >&2
exit 1
fi
echo "OCTOP_VERSION=${VER}" >> "$GITHUB_ENV"
echo "version=${VER}"
- uses: actions/download-artifact@v8
with:
name: dashboard-dist
path: src/octop/dashboard
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
python-version: "3.12"
- uses: actions/setup-node@v5
with:
node-version: "20"
cache: npm
cache-dependency-path: modules/openxyos/package-lock.json
# GitHub Cache outages must not fail the build — PBS download is cheap enough.
- name: Cache python-build-standalone downloads
continue-on-error: true
uses: actions/cache@v5
with:
path: green/.cache
key: pbs-${{ env.PBS_TAG }}-${{ env.PBS_PY }}-${{ matrix.plat }}
- name: Bootstrap portable CPython
env:
GREEN_HOST_PLAT: ${{ matrix.plat }}
run: bash desktop/portable/bootstrap-runtime.sh "${{ matrix.plat }}"
- name: Assemble green zip
env:
GREEN_HOST_PLAT: ${{ matrix.plat }}
# Transitional desktop flavor (0.0.3 iframe bridge, still 0.0.4). Default packaging
# is zero-Node; do not treat this flag as the permanent product.
SKIP_ORG_SIDECAR: "0"
SHIP_OPENXYOS_RUNTIME: "1"
run: |
set -euo pipefail
echo "GREEN_HOST_PLAT=${GREEN_HOST_PLAT} RUNNER_ARCH=${RUNNER_ARCH:-} uname=$(uname -ms)"
bash desktop/portable/package.sh "${{ matrix.plat }}"
- name: Smoke import (native host only)
run: |
set -euo pipefail
staging="desktop/portable/release/FreeOS-${{ matrix.plat }}"
if [[ -x "${staging}/runtime/bin/python3" ]]; then
py="${staging}/runtime/bin/python3"
elif [[ -f "${staging}/runtime/python.exe" ]]; then
py="${staging}/runtime/python.exe"
else
echo "python missing under ${staging}/runtime" >&2
exit 1
fi
req_file="desktop/portable/requirements-${{ matrix.plat }}.txt"
if [[ ! -f "$req_file" ]]; then
echo "frozen requirements missing: ${req_file}" >&2
exit 1
fi
verify_args=(
--packages "${staging}/packages"
--requirements "$req_file"
)
override_file="desktop/portable/overrides-${{ matrix.plat }}.txt"
if [[ -f "$override_file" ]]; then
verify_args+=(--overrides "$override_file")
fi
PYTHONNOUSERSITE=1 \
"$py" desktop/portable/verify_imports.py \
"${verify_args[@]}"
if [[ ! -d "${staging}/org-sidecar" ]]; then
echo "portable zip is missing the managed org-sidecar runtime" >&2
exit 1
fi
echo "managed Organization runtime present"
# macOS: fail if any native extension linked Homebrew/MacPorts paths.
if [[ "${{ matrix.plat }}" == darwin-* ]]; then
REPO_ROOT="$PWD" # shellcheck source=desktop/portable/_common.sh
source desktop/portable/_common.sh
verify_no_homebrew_dylibs "${staging}/packages" "${{ matrix.plat }}"
fi
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
cache-dependency-path: desktop/src/go.sum
- name: Install Linux desktop build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
- name: Install Wails v3 CLI
run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.13
- name: Install NSIS
if: runner.os == 'Windows'
shell: pwsh
run: |
choco install nsis --yes --no-progress
$nsis = "${env:ProgramFiles(x86)}\NSIS"
if (-not (Test-Path "$nsis\makensis.exe")) {
throw "makensis.exe missing after NSIS install"
}
Add-Content -Path $env:GITHUB_PATH -Value $nsis
- name: Package desktop app with bundled portable runtime
working-directory: desktop/src
env:
# Transitional desktop bridge only. Source / Docker stay zero-Node.
SHIP_OPENXYOS_RUNTIME: "1"
run: >-
wails3 task package
ARCH=${{ matrix.arch }}
VERSION=${{ env.OCTOP_VERSION }}
PORTABLE_ZIP=../portable/release/FreeOS-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip
SHIP_OPENXYOS_RUNTIME=1
# archive: false — upload the prebuilt zip as-is. Default archive=true would
# wrap it again, so Actions UI / "Download artifact" becomes zip-in-zip.
# With archive:false, artifact name is the filename (name: is ignored).
- uses: actions/upload-artifact@v7
with:
path: desktop/portable/release/FreeOS-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip
archive: false
if-no-files-found: error
retention-days: 14
- name: Upload bundled desktop package
uses: actions/upload-artifact@v7
with:
path: desktop/src/bin/FreeOS-desktop-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.*
archive: false
if-no-files-found: error
retention-days: 14
release:
name: Attach zips to GitHub Release
needs: package
# always(): still run when package was skipped (attach_from_run rebuild skip).
if: >-
always() &&
!cancelled() &&
(needs.package.result == 'success' || needs.package.result == 'skipped') &&
(
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.attach_from_run != '') ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.attach_release == 'true' &&
github.event.inputs.release_tag != '') ||
(startsWith(github.ref, 'refs/tags/v') &&
(github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.attach_release == 'true')))
)
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- name: Resolve release tag
id: rel
run: |
set -euo pipefail
tag="${{ github.event.inputs.release_tag }}"
if [[ -z "$tag" && "${GITHUB_REF}" == refs/tags/v* ]]; then
tag="${GITHUB_REF_NAME}"
fi
if [[ "$tag" != v* ]]; then
echo "Need a v* tag (run on a version tag, or pass release_tag)." >&2
exit 1
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
# v8 required for archive:false artifacts. skip-decompress keeps the
# uploaded .zip / .tar.gz / .dmg / .exe intact — default unzip turns portable
# zips into directories, so files: release-assets/* would skip them.
- uses: actions/download-artifact@v8
with:
pattern: FreeOS-*
path: release-assets
merge-multiple: true
skip-decompress: true
repository: ${{ github.repository }}
run-id: ${{ github.event.inputs.attach_from_run || github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: List release assets
run: |
set -euo pipefail
ls -lh release-assets/
if find release-assets -mindepth 1 -maxdepth 1 -type d | grep -q .; then
echo "Download extracted archives into directories; refuse incomplete attach." >&2
find release-assets -mindepth 1 -maxdepth 1 -print
exit 1
fi
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.rel.outputs.tag }}
files: release-assets/*
fail_on_unmatched_files: true
# Upsert: works whether Release workflow already created the release.
generate_release_notes: false