diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e029905..92e43a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,20 +8,64 @@ on: workflow_dispatch: permissions: - contents: write + contents: read + +env: + SQLX_OFFLINE: "true" + RELEASE_VERSION: ${{ github.ref_name }} jobs: - build: - runs-on: ubuntu-latest + router: + name: Router (ARMv7 musl) + runs-on: ubuntu-24.04 steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 with: - targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: release-router-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: release-router-${{ runner.os }}-${{ runner.arch }}- + + - name: Cache cross executable + id: cross + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/cross + key: cross-${{ runner.os }}-${{ runner.arch }}-0.2.5 + + - name: Install cross + if: steps.cross.outputs.cache-hit != 'true' + run: cargo install cross --version 0.2.5 --locked + + - name: Build router binaries + run: cross build --release --locked --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent + + - name: Package rootfs tarball + shell: pwsh + run: ./scripts/package_rootfs.ps1 -Version $env:RELEASE_VERSION -Target armv7-unknown-linux-musleabihf + + - name: Upload rootfs + uses: actions/upload-artifact@v4 + with: + name: wakey-rootfs + path: dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz + if-no-files-found: error + compression-level: 0 + + ui: + name: UI + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: @@ -33,8 +77,36 @@ jobs: cache: pnpm cache-dependency-path: ui/pnpm-lock.yaml - - name: Install cross - run: cargo install cross --locked + - name: Build UI dist + working-directory: ui + run: | + pnpm install --frozen-lockfile + pnpm build + + - name: Upload UI + uses: actions/upload-artifact@v4 + with: + name: wakey-ui + path: ui/dist/ + if-no-files-found: error + + cc: + name: CC (${{ matrix.target }}) + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable - name: Cache cargo uses: actions/cache@v4 @@ -43,57 +115,67 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: linux-cargo-${{ hashFiles('**/Cargo.lock') }} + key: release-cc-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: release-cc-${{ matrix.target }}- - - name: Build (armv7-musl with cross) - run: | - cross build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent + # Native GNU builds use the runner's glibc. Gitea remains the separate + # older-glibc release path for deployment hosts that need it. + - name: Build control-plane natively + run: cargo build --release --locked --target ${{ matrix.target }} -p wakey-control-plane - - name: Build UI dist - shell: bash - run: | - set -euo pipefail - cd ui - pnpm install --frozen-lockfile - pnpm build - - - name: Build control-plane (linux-gnu) - shell: bash - run: | - set -euo pipefail - - # x86_64 builds natively; aarch64 needs cross - echo "Building control-plane x86_64-unknown-linux-gnu (native)" - cargo build --release --target x86_64-unknown-linux-gnu -p wakey-control-plane - - echo "Building control-plane aarch64-unknown-linux-gnu (cross)" - cross build --release --target aarch64-unknown-linux-gnu -p wakey-control-plane - - - name: Package rootfs tarball - shell: pwsh - run: | - ./scripts/package_rootfs.ps1 -Version "${{ github.ref_name }}" -Target armv7-unknown-linux-musleabihf - - - name: Package control-plane bundles - shell: bash - run: | - set -euo pipefail - chmod +x scripts/update_wakey_cc.sh scripts/package_wakey_cc_bundle.sh - for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do - ./scripts/package_wakey_cc_bundle.sh --version "${{ github.ref_name }}" --target "$target" - done - - - name: Upload artifact + - name: Upload control-plane binary uses: actions/upload-artifact@v4 + with: + name: wakey-cc-bin-${{ matrix.target }} + path: target/${{ matrix.target }}/release/wakey-control-plane + if-no-files-found: error + + release: + name: Package and publish + needs: [router, ui, cc] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download rootfs + uses: actions/download-artifact@v4 with: name: wakey-rootfs - path: dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz + path: dist + + - name: Download UI + uses: actions/download-artifact@v4 + with: + name: wakey-ui + path: ui/dist + + - name: Download x86-64 CC + uses: actions/download-artifact@v4 + with: + name: wakey-cc-bin-x86_64-unknown-linux-gnu + path: target/x86_64-unknown-linux-gnu/release + + - name: Download ARM64 CC + uses: actions/download-artifact@v4 + with: + name: wakey-cc-bin-aarch64-unknown-linux-gnu + path: target/aarch64-unknown-linux-gnu/release + + - name: Package control-plane bundles + run: | + for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do + sh ./scripts/package_wakey_cc_bundle.sh --version "$RELEASE_VERSION" --target "$target" + done - name: Upload control-plane bundles uses: actions/upload-artifact@v4 with: name: wakey-cc-bundles path: dist/wakey-cc-*unknown-linux-gnu.tgz + if-no-files-found: error + compression-level: 0 - name: Publish Release to GitHub if: startsWith(github.ref, 'refs/tags/') @@ -101,6 +183,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref_name }} + fail_on_unmatched_files: true files: | dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz dist/wakey-cc-*unknown-linux-gnu.tgz diff --git a/scripts/README.md b/scripts/README.md index a7342c2..5e4ad8b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -144,6 +144,14 @@ Implementation notes: ## CI (GitHub) +- `.github/workflows/release.yml` builds router binaries, UI, x86-64 CC and + ARM64 CC in parallel. CC uses matching Ubuntu 24.04 runners and native Cargo; + Cross is used only for the ARMv7-musl router binaries. A final job downloads + their artifacts, packages CC bundles and publishes after every build succeeds. +- GitHub's native GNU binaries inherit its runner's glibc requirements. Use the + Gitea bundles for older deployment hosts, checking that its build actually + used Zig's configured baseline. Gitea retains one job for `--once` runners. + - Defined in `.github/workflows/ci.yml` on pushes/PRs to `main` or `master`: - **`rust` job:** `cargo fmt --check`, `clippy -D warnings`, `cargo test --workspace` on `ubuntu-latest`. - **`ui` job:** under `ui/`, `pnpm install --frozen-lockfile`, then `pnpm run typecheck`, `format:check`, and `build` (mirrors what you need before packaging the control-plane bundle). GitHub sets `CI=true` for the runner; pnpm uses lockfile `ui/pnpm-lock.yaml` for cache. diff --git a/scripts/WSL_RUNNER.md b/scripts/WSL_RUNNER.md index 52cbaf7..2698a43 100644 --- a/scripts/WSL_RUNNER.md +++ b/scripts/WSL_RUNNER.md @@ -67,6 +67,12 @@ From Windows PowerShell, using the WSL wrapper: Path control (binary and config are independent): +When called from PowerShell inside WSL, absolute POSIX paths pass through +unchanged. Windows paths are converted using the selected distro's `wslpath`. +Relative runner home, binary and config paths resolve from the current PowerShell +directory. Conversion also supports paths that have not been created yet. +The helper, runner home and config paths must be accessible in that distro. + ```powershell ./scripts/act_runner_wsl.ps1 -Action status ` -RunnerBin /home/lda/gitea-runner/act_runner ` diff --git a/scripts/act_runner_wsl.ps1 b/scripts/act_runner_wsl.ps1 index d82321b..fc1c9ec 100755 --- a/scripts/act_runner_wsl.ps1 +++ b/scripts/act_runner_wsl.ps1 @@ -2,7 +2,7 @@ param( [ValidateSet("update", "register", "start", "stop", "status")] [string]$Action = "start", - [string]$Distro = $(if ($env:WAKEY_WSL_DISTRO) { $env:WAKEY_WSL_DISTRO } else { "FedoraLinux-43" }), + [string]$Distro = $( $env:WAKEY_WSL_DISTRO ?? "FedoraLinux-43" ), [string]$RunnerHome, [string]$RunnerBin = "/home/lda/gitea-runner/act_runner", [string]$ConfigPath, @@ -22,7 +22,7 @@ if (-not (Test-Path $scriptWin)) { throw "Missing Fedora runner helper: $scriptWin" } -$scriptLinux = (& wsl.exe -d $Distro -e wslpath -a "$scriptWin").Trim() +$scriptLinux = ConvertTo-WslPath -WindowsPath $scriptWin -Distro $Distro if (-not $scriptLinux) { throw "Failed to resolve WSL path for $scriptWin" } @@ -30,21 +30,19 @@ if (-not $scriptLinux) { if (-not $RunnerHome) { $repoRootWin = Split-Path -Parent $PSScriptRoot $defaultRunnerHomeWin = Join-Path $repoRootWin "ar_data/wsl_runner" - $RunnerHome = (& wsl.exe -d $Distro -e wslpath -a "$defaultRunnerHomeWin").Trim() - if (-not $RunnerHome) { - throw "Failed to resolve WSL runner home for $defaultRunnerHomeWin" - } + $RunnerHome = $defaultRunnerHomeWin } if (-not $ConfigPath) { $repoRootWin = Split-Path -Parent $PSScriptRoot $defaultConfigWin = Join-Path $repoRootWin "ar_data/wsl_runner/config.yaml" - $ConfigPath = (& wsl.exe -d $Distro -e wslpath -a "$defaultConfigWin").Trim() - if (-not $ConfigPath) { - throw "Failed to resolve WSL config path for $defaultConfigWin" - } + $ConfigPath = $defaultConfigWin } +$RunnerHome = ConvertTo-WslPath -Path $RunnerHome -Distro $Distro +$RunnerBin = ConvertTo-WslPath -Path $RunnerBin -Distro $Distro +$ConfigPath = ConvertTo-WslPath -Path $ConfigPath -Distro $Distro + $parts = @("bash", (Quote-ShArg $scriptLinux), (Quote-ShArg $Action), "--runner-home", (Quote-ShArg $RunnerHome)) $parts += @("--runner-bin", (Quote-ShArg $RunnerBin), "--config-path", (Quote-ShArg $ConfigPath)) if ($ServerUrl) { $parts += @("--server-url", (Quote-ShArg $ServerUrl)) } diff --git a/scripts/lib.ps1 b/scripts/lib.ps1 index 3489a98..e77f34a 100755 --- a/scripts/lib.ps1 +++ b/scripts/lib.ps1 @@ -27,8 +27,15 @@ function Test-IsWsl { return $false } +# With -Distro, resolve local relative paths from the PowerShell working +# directory and convert Windows paths with the distro's mount configuration. +# Without it, keep the existing mapping used by SCP callers, including remote destinations. function ConvertTo-WslPath { - param([string]$WindowsPath) + param( + [Alias('Path')] + [string]$WindowsPath, + [string]$Distro + ) if ([string]::IsNullOrWhiteSpace($WindowsPath)) { return $WindowsPath @@ -39,6 +46,36 @@ function ConvertTo-WslPath { return $WindowsPath } + # wslpath expects a Windows input here. Use the selected distro's mount + # configuration when requested, while keeping existing POSIX paths intact. + if ($Distro) { + # Absolute Windows paths can be supplied even from Linux PowerShell. + # Relative inputs use the filesystem provider without requiring a file + # to exist, which is important for a new runner home/config. + if ($WindowsPath -notmatch '^[A-Za-z]:[\\/]|^\\\\') { + $provider = $null + $drive = $null + $WindowsPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + $WindowsPath, [ref]$provider, [ref]$drive) + if ($provider.Name -ne 'FileSystem') { + throw "WSL paths must use the filesystem provider" + } + if ($WindowsPath.StartsWith('/')) { + return $WindowsPath + } + } + $output = & wsl.exe -d $Distro -e wslpath -a "$WindowsPath" + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + throw "wslpath failed for '$WindowsPath' in '$Distro' (exit $exitCode)" + } + $converted = ($output -join "`n").Trim() + if (-not $converted) { + throw "wslpath returned no path for '$WindowsPath' in '$Distro'" + } + return $converted + } + # Handle Windows path (e.g., C:\path\to\file -> /mnt/c/path/to/file) if ($WindowsPath -match '^([A-Z]):(.*)$') { $drive = $matches[1].ToLower() diff --git a/scripts/tests/wsl_paths.ps1 b/scripts/tests/wsl_paths.ps1 new file mode 100644 index 0000000..0023104 --- /dev/null +++ b/scripts/tests/wsl_paths.ps1 @@ -0,0 +1,97 @@ +#!/usr/bin/env pwsh +# Run with pwsh -NoProfile -File scripts/tests/wsl_paths.ps1. +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot/../lib.ps1" + +# Intercept WSL so these tests never start or register a real runner. +$global:WakeyWslTest = @{ + Calls = [System.Collections.Generic.List[object]]::new() + ExitCode = 0 + # Fake conversion response, chosen to exercise a path containing spaces. + Output = '/mnt/c/runner home/config.yaml' +} +function global:wsl.exe { + $global:WakeyWslTest.Calls.Add(@($args)) + $global:LASTEXITCODE = $global:WakeyWslTest.ExitCode + if ($args[3] -eq 'wslpath') { $global:WakeyWslTest.Output } +} + +foreach ($path in @('/home/lda/repo/scripts/helper.sh', '/mnt/c/runner home/config.yaml')) { + $result = ConvertTo-WslPath -WindowsPath $path -Distro TestDistro + if ($result -cne $path) { throw "POSIX path changed: $result" } +} +if ($global:WakeyWslTest.Calls.Count -ne 0) { throw 'POSIX paths invoked wslpath' } + +foreach ($path in @('C:\runner home\config.yaml', 'C:/runner home/config.yaml', '\\server\share\runner home\config.yaml')) { + $global:WakeyWslTest.Calls.Clear() + $result = ConvertTo-WslPath -WindowsPath $path -Distro TestDistro + if ($result -cne '/mnt/c/runner home/config.yaml') { throw 'Mock Windows conversion failed' } + $call = $global:WakeyWslTest.Calls[0] + if (($call[0..4] -join ' ') -cne '-d TestDistro -e wslpath -a' -or $call[5] -cne $path) { + throw 'Distro or Windows path was not passed intact' + } +} +if ((ConvertTo-WslPath 'D:\runner\config.yaml') -cne '/mnt/d/runner/config.yaml') { + throw 'Existing drive conversion changed' +} +foreach ($path in @('root@router:/root/.bin/', 'relative/file')) { + if ((ConvertTo-WslPath $path) -cne $path) { throw 'Existing SCP path handling changed' } +} + +# Resolve missing relative paths from the PS location, including dot segments. +$temp = Join-Path ([System.IO.Path]::GetTempPath()) "wakey-wsl-paths-$([guid]::NewGuid())" +New-Item -ItemType Directory -Path $temp | Out-Null +Push-Location $temp +try { + foreach ($relative in @('new folder/config.yaml', '.\new folder\config.yaml', 'nested/../new folder/config.yaml')) { + $expected = Join-Path $temp 'new folder/config.yaml' + $global:WakeyWslTest.Calls.Clear() + $result = ConvertTo-WslPath -Path $relative -Distro TestDistro + if ($IsWindows) { + if ($global:WakeyWslTest.Calls[0][5] -cne $expected) { throw 'Relative Windows path was not resolved before conversion' } + } elseif ($result -cne $expected -or $global:WakeyWslTest.Calls.Count -ne 0) { + throw "Relative POSIX path was not resolved: $result" + } + } +} finally { + Pop-Location + Remove-Item -Recurse -Force $temp +} + +foreach ($case in @( + @{ ExitCode = 1; Output = '' }, + @{ ExitCode = 1; Output = '/mnt/c/bad' }, + @{ ExitCode = 0; Output = '' } +)) { + $global:WakeyWslTest.ExitCode = $case.ExitCode + $global:WakeyWslTest.Output = $case.Output + $caught = $false + try { ConvertTo-WslPath -WindowsPath 'C:\bad' -Distro TestDistro | Out-Null } + catch { $caught = $_.Exception.Message -like 'wslpath *' } + if (-not $caught) { throw 'Failed/empty wslpath result was accepted' } +} + +if (-not $IsWindows) { + $global:WakeyWslTest.Calls.Clear() + $global:WakeyWslTest.ExitCode = 0 + & "$PSScriptRoot/../act_runner_wsl.ps1" -Action status -Distro TestDistro + if ($global:WakeyWslTest.Calls.Count -ne 1 -or $global:WakeyWslTest.Calls[0][3] -ne 'bash') { + throw 'POSIX wrapper paths should invoke only the runner command' + } + $command = $global:WakeyWslTest.Calls[0][5] + if ($command -notlike '*act_runner_fedora.sh*' -or $command -notlike '*ar_data/wsl_runner/config.yaml*') { + throw 'Wrapper did not resolve default paths' + } + $global:WakeyWslTest.Calls.Clear() + $global:WakeyWslTest.Output = '/mnt/c/runner home/config.yaml' + & "$PSScriptRoot/../act_runner_wsl.ps1" -Action status -Distro TestDistro ` + -RunnerHome 'new runner home' -RunnerBin 'bin/act_runner' -ConfigPath 'C:\runner home\config.yaml' + if ($global:WakeyWslTest.Calls.Count -ne 2 -or $global:WakeyWslTest.Calls[0][3] -ne 'wslpath') { + throw 'Explicit Windows config should be converted once before running bash' + } + $command = $global:WakeyWslTest.Calls[1][5] + foreach ($path in @((Join-Path (Get-Location).Path 'new runner home'), (Join-Path (Get-Location).Path 'bin/act_runner'), '/mnt/c/runner home/config.yaml')) { + if (-not $command.Contains((Quote-ShArg $path))) { throw "Explicit path missing from runner command: $path" } + } +} +Write-Host 'WSL path tests passed'