CI #294
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: CI | |
| on: | |
| push: | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| windows_release_tag: | |
| description: "Tag to attach an UNSIGNED Windows installer to (e.g. v1.1.0). Blank = skip." | |
| required: false | |
| default: "" | |
| linux_release_tag: | |
| description: "Tag to attach an UNSIGNED Linux AppImage/deb to (e.g. v1.5.0). Blank = skip." | |
| required: false | |
| default: "" | |
| jobs: | |
| test: | |
| name: Test (${{ matrix.name }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: Linux | |
| os: ubuntu-latest | |
| - name: macOS Apple Silicon | |
| os: macos-15 | |
| - name: macOS Intel | |
| os: macos-15-intel | |
| - name: Windows | |
| os: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run ci | |
| # The native macOS bridge daemon gates Screen Watch / creative-app / | |
| # editor actions, so its Swift unit tests must pass before changes land. | |
| # The bridge is macOS-only and needs the Xcode Swift toolchain | |
| # (preinstalled on GitHub macOS runners), so this step is gated to the | |
| # macOS legs of the matrix and skipped on Linux/Windows. `swift test` | |
| # exits non-zero on failure, which fails this job. | |
| - name: Swift bridge tests (macOS only) | |
| if: runner.os == 'macOS' | |
| run: | | |
| swift --version | |
| npm run test:swift:bridge | |
| # iOS companion gate — the Electron matrix can't see iOS regressions, and the | |
| # phone is the monetized launch surface. macOS 26 runner for the iOS 26 SDK | |
| # (TaskWraithUI uses `.glassEffect`), with Xcode PINNED to 26.0 (Swift 6.2) to | |
| # MATCH the shipping toolchain: the runner's default Xcode 26.5 (Swift 6.3.2) | |
| # enforces region-isolation `sending` diagnostics as errors that local | |
| # Swift 6.2.4 does not, so an unpinned lane would fail on latent concurrency | |
| # debt rather than real regressions. (TODO: clear that Swift-6.3 region- | |
| # isolation debt in RemoteSessionModel before bumping the pin.) | |
| ios: | |
| name: iOS (Kit tests) | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Available Xcodes (pre-pin diagnostic) | |
| run: ls -d /Applications/Xcode*.app | |
| - name: Select Xcode 26.0 (Swift 6.2 — matches ship toolchain) | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: "26.0" | |
| - name: Toolchain versions | |
| run: xcodebuild -version && swift --version | |
| # `swift test` builds TaskWraithKit + TaskWraithUI (all the SwiftUI/Glass | |
| # code) for the host and runs the 73 unit tests — the real regression | |
| # gate. An app-target `xcodebuild` is intentionally NOT run here: the | |
| # pinned Xcode 26.0 lacks the iOS simulator PLATFORM (GitHub pre-installs | |
| # it only for the runner's default Xcode 26.5), and `-downloadPlatform iOS` | |
| # would add minutes + GBs per run just to compile the thin app wrapper. | |
| - name: Swift package tests (TaskWraithKit) | |
| run: swift test --package-path ios/TaskWraithKit | |
| notarized-macos-release: | |
| name: Notarized macOS Release | |
| runs-on: macos-15 | |
| needs: [test, ios] | |
| if: startsWith(github.ref, 'refs/tags/v') && vars.ENABLE_MACOS_NOTARIZED_RELEASE == 'true' | |
| permissions: | |
| contents: write | |
| env: | |
| CSC_LINK: ${{ secrets.MACOS_CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }} | |
| CSC_NAME: ${{ secrets.MACOS_CSC_NAME }} | |
| APPLE_KEYCHAIN_PROFILE: ${{ secrets.APPLE_KEYCHAIN_PROFILE }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run security:deps | |
| - run: npm run build:mac:notarized | |
| - name: Verify notarized macOS artifacts | |
| run: | | |
| set -euo pipefail | |
| test -d "dist/mac-universal/TaskWraith.app" | |
| codesign --verify --deep --strict --verbose=2 "dist/mac-universal/TaskWraith.app" | |
| spctl --assess --type execute --verbose=4 "dist/mac-universal/TaskWraith.app" | |
| xcrun stapler validate "dist/mac-universal/TaskWraith.app" | |
| for artifact in dist/*.dmg; do | |
| test -f "$artifact" | |
| xcrun stapler validate "$artifact" | |
| done | |
| - name: Upload verified macOS release artifacts | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| assets=(dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml dist/sbom.cdx.json) | |
| if [ "${#assets[@]}" -lt 3 ]; then | |
| echo "Expected notarized macOS release artifacts in dist/, found ${#assets[@]}." | |
| exit 1 | |
| fi | |
| gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || \ | |
| gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes "Verified TaskWraith release artifacts for $GITHUB_REF_NAME." || \ | |
| gh release view "$GITHUB_REF_NAME" >/dev/null | |
| gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber | |
| signed-windows-release: | |
| name: Signed Windows Release | |
| runs-on: windows-latest | |
| needs: [test, ios] | |
| if: startsWith(github.ref, 'refs/tags/v') && vars.ENABLE_WINDOWS_SIGNED_RELEASE == 'true' | |
| permissions: | |
| contents: write | |
| env: | |
| CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run security:deps | |
| - run: npm run build:win:signed | |
| - name: Verify signed Windows artifacts | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $installers = Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe" | |
| if ($installers.Count -lt 2) { | |
| throw "Expected signed x64 and arm64 Windows installers in dist/." | |
| } | |
| foreach ($installer in $installers) { | |
| $signature = Get-AuthenticodeSignature -FilePath $installer.FullName | |
| if ($signature.Status -ne "Valid") { | |
| throw "Invalid Authenticode signature for $($installer.Name): $($signature.Status)" | |
| } | |
| } | |
| $version = (Get-Content package.json | ConvertFrom-Json).version | |
| $feedPrefix = if ($version -match "-") { "beta" } else { "latest" } | |
| foreach ($arch in @("x64", "arm64")) { | |
| $feed = "dist/$feedPrefix-win-$arch.yml" | |
| if (!(Test-Path $feed)) { | |
| throw "Missing $feed" | |
| } | |
| } | |
| - name: Upload verified Windows release artifacts | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $assets = @( | |
| Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe" | |
| Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe.blockmap" | |
| Get-ChildItem -Path dist -Filter "*-win-*.yml" | |
| ) | ForEach-Object { $_.FullName } | |
| if ($assets.Count -lt 4) { | |
| throw "Expected signed Windows installers, blockmaps, and update feeds in dist/, found $($assets.Count)." | |
| } | |
| gh release view "$env:GITHUB_REF_NAME" *> $null | |
| if ($LASTEXITCODE -ne 0) { | |
| gh release create "$env:GITHUB_REF_NAME" --title "$env:GITHUB_REF_NAME" --notes "Verified TaskWraith release artifacts for $env:GITHUB_REF_NAME." | |
| if ($LASTEXITCODE -ne 0) { | |
| gh release view "$env:GITHUB_REF_NAME" *> $null | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Could not create or verify GitHub release $env:GITHUB_REF_NAME." | |
| } | |
| } | |
| } | |
| gh release upload "$env:GITHUB_REF_NAME" @assets --clobber | |
| unsigned-windows-build: | |
| name: Unsigned Windows Build (testing) | |
| runs-on: windows-latest | |
| needs: [test, ios] | |
| if: github.event_name == 'workflow_dispatch' && inputs.windows_release_tag != '' | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run security:deps | |
| # `build:win:nopublish` shares build:win's recipe (compile + electron-builder | |
| # + packaged-app smoke + update-feed write/validate) but forces | |
| # `--publish never`: electron-builder's implicit CI publish would otherwise | |
| # try to attach to the `windows_release_tag` release and refuse any release | |
| # created >2h ago. The packaged-app launch smoke now skips the arm64 binary | |
| # on this x64 runner (it can't be executed natively) instead of crashing | |
| # with "spawn UNKNOWN", so it is safe to run here. Assets are attached by the | |
| # gh step below (gh release upload --clobber works on an existing release). | |
| - run: npm run build:win:nopublish | |
| - name: Upload unsigned Windows installers to the release | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $tag = "${{ inputs.windows_release_tag }}" | |
| $assets = @( | |
| Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe" | |
| Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe.blockmap" | |
| Get-ChildItem -Path dist -Filter "*-win-*.yml" | |
| ) | ForEach-Object { $_.FullName } | |
| if ($assets.Count -lt 2) { | |
| throw "Expected Windows installers in dist/, found $($assets.Count)." | |
| } | |
| Write-Host "Uploading $($assets.Count) asset(s) to release $tag" | |
| gh release upload $tag @assets --clobber | |
| unsigned-linux-build: | |
| name: Unsigned Linux Build (testing) | |
| runs-on: ubuntu-latest | |
| needs: [test, ios] | |
| if: github.event_name == 'workflow_dispatch' && inputs.linux_release_tag != '' | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run security:deps | |
| # AppImage + deb only — snap needs snapcraft (absent on the default | |
| # runner). `--publish never` stops electron-builder's implicit CI publish | |
| # from trying to attach to a release created >2h ago; the gh step below | |
| # attaches the assets (gh release upload --clobber on an existing release). | |
| - run: npm run build:linux:nopublish | |
| - name: Upload unsigned Linux artifacts to the release | |
| run: | | |
| set -euo pipefail | |
| tag="${{ inputs.linux_release_tag }}" | |
| shopt -s nullglob | |
| assets=(dist/*.AppImage dist/*.deb dist/latest-linux*.yml) | |
| if [ ${#assets[@]} -lt 1 ]; then | |
| echo "Expected Linux artifacts in dist/, found none." | |
| exit 1 | |
| fi | |
| echo "Uploading ${#assets[@]} asset(s) to release $tag" | |
| gh release upload "$tag" "${assets[@]}" --clobber |