fix: make desktop updates transactional and restart the shell #169
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |