" << safeTitle << "
" << safeDetail << "
diff --git a/.agent/workflows/project-settings.md b/.agent/workflows/project-settings.md index 80dd6d9..73fc9ad 100644 --- a/.agent/workflows/project-settings.md +++ b/.agent/workflows/project-settings.md @@ -1,14 +1,16 @@ --- -description: Project-specific settings and preferences for Studio13-v3 +description: Project-specific settings and preferences for OpenStudio --- # Project Settings ## Build Verification -- **Do NOT auto-run build verification commands** after making code changes -- **Do NOT proactively check if builds work** - the user will inform you if something is broken -- Only run build commands when explicitly requested by the user +- Verify every code change in proportion to its risk before handing it off. +- Frontend changes require targeted tests plus `npm run build`; native changes + require the relevant CMake Debug build and deterministic regression harnesses. +- Never treat a successful incremental build as proof that a clean dependency + install or clean CI build works. ## Error Handling @@ -88,16 +90,17 @@ All UI components are located in `frontend/src/components/ui/` and should be use ### Component Reference Files -- [Button.tsx](frontend/src/components/ui/Button/Button.tsx) - Full JSDoc with examples -- [Input.tsx](frontend/src/components/ui/Input/Input.tsx) -- [Select.tsx](frontend/src/components/ui/Select/Select.tsx) -- [Checkbox.tsx](frontend/src/components/ui/Checkbox/Checkbox.tsx) -- [Textarea.tsx](frontend/src/components/ui/Textarea/Textarea.tsx) -- [Slider.tsx](frontend/src/components/ui/Slider/Slider.tsx) -- [Modal.tsx](frontend/src/components/ui/Modal/Modal.tsx) +- [Button.tsx](../../frontend/src/components/ui/Button/Button.tsx) - Full JSDoc with examples +- [Input.tsx](../../frontend/src/components/ui/Input/Input.tsx) +- [Select.tsx](../../frontend/src/components/ui/Select/Select.tsx) +- [Checkbox.tsx](../../frontend/src/components/ui/Checkbox/Checkbox.tsx) +- [Textarea.tsx](../../frontend/src/components/ui/Textarea/Textarea.tsx) +- [Slider.tsx](../../frontend/src/components/ui/Slider/Slider.tsx) +- [Modal.tsx](../../frontend/src/components/ui/Modal/Modal.tsx) ## General Workflow - Make the requested changes directly -- Trust the user to test and report any issues +- Run the automated checks that cover the changed behavior before asking the + user to perform subjective or hardware-dependent testing - Keep explanations concise unless the user asks for details diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 18c09b8..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npx tsc:*)", - "Bash(npm run build:*)", - "Bash(cmake --build:*)", - "Bash(ls:*)", - "Bash(npm run dev:*)", - "Bash(npm list:*)", - "Bash(where:*)", - "Bash(npx:*)", - "Bash(node -e:*)", - "Bash(npm install:*)", - "Bash(node:*)", - "Bash(dir:*)", - "Bash(python -c:*)", - "Bash(python:*)", - "Bash(# Check if there are leftover Studio13 processes or WebView2 processes tasklist)", - "Bash(echo:*)", - "Read(//c/Users/srvds/AppData/Local/Temp/**)", - "Read(//c/Users/srvds/Documents/Studio13/**)", - "Read(//c/Users/srvds/Documents/**)", - "Read(//c/Users/srvds/OneDrive/Documents/Studio13/**)", - "Read(//c/Users/srvds/**)", - "Bash(find . -name \"*.tsx\" -type f -exec grep -l \"useSortable\\\\|@dnd-kit\" {} ;)", - "WebFetch(domain:forums.steinberg.net)", - "Bash(export PATH=\"$PATH:/c/Program Files/NASM\")", - "Bash(cmake -B build -DCMAKE_BUILD_TYPE=Debug)", - "WebSearch", - "Bash(gh release:*)", - "Bash(find /c/Users/srvds/Documents/Codes/Studio13-v3/resources -type f -name *model* -o -name *.onnx -o -name *.ckpt)", - "Bash(find Source -type f \\\\\\( -name \"*.cpp\" -o -name \"*.h\" \\\\\\) -exec wc -l {} +)", - "Bash(find \"c:/Users/srvds/Documents/Codes/Studio13-v3/frontend/src\" -type f \\\\\\( -name \"*.tsx\" -o -name \"*.ts\" -o -name \"*.css\" \\\\\\) -exec wc -l {} +)", - "Bash(find /c/Users/srvds/Documents/Codes/Studio13-v3/Source -type f \\\\\\( -name \"*.cpp\" -o -name \"*.h\" \\\\\\) -exec wc -l {} +)", - "Bash(find /c/Users/srvds/Documents/Codes/Studio13-v3/frontend/src -type f \\\\\\( -name \"*.ts\" -o -name \"*.tsx\" \\\\\\) -exec wc -l {} +)", - "Bash(find /c/Users/srvds/Documents/Codes/Studio13-v3 -type f \\\\\\(-name *test* -o -name *spec* -o -name .github -o -name .gitlab-ci* -o -name azure-pipelines* \\\\\\))", - "Bash(du -sh /c/Users/srvds/Documents/Codes/Studio13-v3/*)", - "Bash(xargs grep:*)", - "Bash(for file:*)", - "Bash(do echo:*)", - "Read(//c/Users/srvds/Documents/Codes/Studio13-v3/**)", - "Bash(done)", - "Bash(wc -l Source/*.cpp Source/*.h)", - "Bash(grep -o \"include.*signalsmith\\\\|include.*ysfx\\\\|include.*lua\\\\|include.*onnx\\\\|include.*clap\" Source/*.h)", - "Bash(grep -r \"^[[:space:]]*\\\\/\\\\/\" Source/*.cpp)", - "Bash(grep -h \"^[[:space:]]*\\\\/\\\\*\" Source/*.cpp)", - "Bash(find /c/Users/srvds/Documents/Codes/Studio13-v3 -type d -iname *test* -o -type d -iname *spec*)" - ], - "additionalDirectories": [ - "C:\\Users\\srvds\\OneDrive\\Documents\\Studio13" - ] - } -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f247850 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Unified-diff context lines contain one required leading space. Treat those +# syntax markers as valid instead of reporting them as trailing whitespace. +*.patch text eol=lf whitespace=-blank-at-eol +*.onnx binary +thirdparty/basic-pitch/LICENSE text eol=lf +thirdparty/basic-pitch/NOTICE text eol=lf +thirdparty/ffmpeg/COPYING.GPLv3 text eol=lf +thirdparty/ffmpeg/PROVENANCE.json text eol=lf +thirdparty/signalsmith/LICENSE.txt text eol=lf +thirdparty/signalsmith/signalsmith-linear/LICENSE.txt text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..077387b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default ownership for every path in OpenStudio. +* @sdevil7th diff --git a/.github/ISSUE_TEMPLATE/window-or-startup-failure.yml b/.github/ISSUE_TEMPLATE/window-or-startup-failure.yml new file mode 100644 index 0000000..57ed42b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/window-or-startup-failure.yml @@ -0,0 +1,115 @@ +name: Window or embedded UI failure +description: Report startup, Mixer, MIDI editor, or built-in plug-in window failures. +title: "[Window/UI]: " +labels: [] +body: + - type: markdown + attributes: + value: | + Thanks for helping test OpenStudio on real hardware. The OS/build details and startup log below let us distinguish a missing runtime from a WebView/WKWebView lifecycle failure. + + - type: input + id: version + attributes: + label: OpenStudio version + description: Use the release/tag shown in the app or the exact commit hash for a development build. + placeholder: v0.0.40 or commit abc1234 + validations: + required: true + + - type: dropdown + id: platform + attributes: + label: Operating system + options: + - Windows 10 + - Windows 11 + - macOS (Apple Silicon) + - macOS (Intel) + - Linux + - Other + validations: + required: true + + - type: input + id: os_version + attributes: + label: Exact OS version and build + description: On Windows, run `winver`. On macOS, use About This Mac. + placeholder: Windows 10 22H2 build 19045.6466; macOS 14.7.6 + validations: + required: true + + - type: dropdown + id: window + attributes: + label: Affected window + options: + - Main application startup + - Detached Mixer + - Detached MIDI editor + - Built-in plug-in editor + - Native VST3/CLAP/AU plug-in editor + - Other + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: Include open/close/dock/reopen order, whether this was the first launch, and whether another OpenStudio window was still loading. + placeholder: | + 1. Launch OpenStudio + 2. Open ... + 3. Close ... immediately + 4. Reopen ... + validations: + required: true + + - type: textarea + id: observed + attributes: + label: What happened? + description: Say whether the window was blank, never appeared, froze, crashed, or showed a recovery message. + validations: + required: true + + - type: textarea + id: startup_log + attributes: + label: Startup log + description: Paste or attach `OpenStudio_Startup.log`. On Windows it is under `%APPDATA%\OpenStudio\logs`; on macOS it is under `~/Library/Application Support/OpenStudio/logs`. Remove any private project paths if needed. + render: text + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Additional environment details + description: On Windows include the WebView2 version, SmartScreen/Smart App Control result, and any Controlled Folder Access, antivirus, EDR, AppLocker, or WDAC policy. On macOS include Intel/Apple Silicon, whether the DMG/app was signed and notarized, the exact Gatekeeper message, how launch was approved, whether quarantine was removed, and whether Rosetta is installed. + + - type: dropdown + id: trust_state + attributes: + label: Download and first-launch state + options: + - Signed/notarized or verified-publisher build; no warning + - Warning shown; approved with the operating system UI + - macOS quarantine removed with xattr + - Windows security or organization policy blocked launch + - Local development build; no download quarantine + - Unknown + validations: + required: true + + - type: checkboxes + id: checks + attributes: + label: Confirmation + options: + - label: I tested a current release or included the exact development commit. + required: true + - label: I included the complete startup section of the log, including browser backend and frontend startup state. + required: true diff --git a/.github/workflows/ai-runtime-release.yml b/.github/workflows/ai-runtime-release.yml index c3409b4..c4876c3 100644 --- a/.github/workflows/ai-runtime-release.yml +++ b/.github/workflows/ai-runtime-release.yml @@ -10,6 +10,9 @@ on: description: "AI runtime version, for example 0.0.30" required: true +permissions: + contents: read + jobs: build-windows-runtime-base: runs-on: windows-latest @@ -19,7 +22,7 @@ jobs: AI_RUNTIME_STANDALONE_PYTHON_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION || '3.10.20' }} AI_RUNTIME_STANDALONE_FLAVOR: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR || 'install_only' }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize runtime version shell: pwsh @@ -28,7 +31,7 @@ jobs: "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Cache standalone Python and wheels - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | .cache/ai-runtime @@ -59,7 +62,7 @@ jobs: -ExpectedRuntimeVersion $env:VERSION ` -MaxArtifactSizeBytes 2100000000 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-base-ai-runtime path: | @@ -75,14 +78,14 @@ jobs: AI_RUNTIME_STANDALONE_PYTHON_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION || '3.10.20' }} AI_RUNTIME_STANDALONE_FLAVOR: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR || 'install_only' }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize runtime version shell: bash run: echo "VERSION=${VERSION#ai-runtime-v}" | sed 's/^VERSION=v/VERSION=/' >> "$GITHUB_ENV" - name: Cache standalone Python and wheels - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | .cache/ai-runtime @@ -91,7 +94,7 @@ jobs: - name: Cache packaged macOS runtime id: cache-macos-runtime-package - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | dist/ai-runtime/OpenStudio-AI-Runtime-macos-arm64.zip @@ -123,7 +126,7 @@ jobs: -ExpectedRuntimeVersion $env:VERSION ` -MaxArtifactSizeBytes 2100000000 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: macos-arm64-ai-runtime path: | @@ -139,14 +142,14 @@ jobs: AI_RUNTIME_STANDALONE_PYTHON_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION || '3.10.20' }} AI_RUNTIME_STANDALONE_FLAVOR: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR || 'install_only' }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize runtime version shell: bash run: echo "VERSION=${VERSION#ai-runtime-v}" | sed 's/^VERSION=v/VERSION=/' >> "$GITHUB_ENV" - name: Cache standalone Python and wheels - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | .cache/ai-runtime @@ -177,7 +180,7 @@ jobs: -ExpectedRuntimeVersion $env:VERSION ` -MaxArtifactSizeBytes 2100000000 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: linux-cpu-x64-ai-runtime path: | @@ -196,23 +199,23 @@ jobs: env: VERSION: ${{ github.event.inputs.runtime_version || github.ref_name }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize runtime version shell: bash run: echo "VERSION=${VERSION#ai-runtime-v}" | sed 's/^VERSION=v/VERSION=/' >> "$GITHUB_ENV" - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: windows-base-ai-runtime path: dist/ai-runtime - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: macos-arm64-ai-runtime path: dist/ai-runtime - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: linux-cpu-x64-ai-runtime path: dist/ai-runtime @@ -228,14 +231,13 @@ jobs: test -f "dist/ai-runtime/reports/linux-cpu-x64-size-report.json" - name: Publish AI runtime release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('ai-runtime-v{0}', github.event.inputs.runtime_version) }} name: OpenStudio AI Runtime ${{ env.VERSION }} body: | OpenStudio AI base runtimes. Windows accelerator backends are installed on-device from the pinned install plans in the published runtime manifest. fail_on_unmatched_files: true - overwrite_files: true files: | dist/ai-runtime/OpenStudio-AI-Runtime-windows-base-x64.zip dist/ai-runtime/OpenStudio-AI-Runtime-macos-arm64.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c470e8..11f13fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,9 @@ on: required: false default: "packaging/release-notes-template.md" +permissions: + contents: read + jobs: build-windows: runs-on: windows-latest @@ -22,35 +25,16 @@ jobs: BUILD_DIR: build-release-windows ASIO_SDK_DIR: thirdparty/asio ONNXRUNTIME_VERSION: 1.24.4 + ONNXRUNTIME_WIN_X64_SHA256: d2319fddfb6ea4db99ccc4b60c85c517bcd855721f5daa6a06d40d7cb2ee2357 + FFMPEG_CORRESPONDING_SOURCE_URL: ${{ vars.OPENSTUDIO_FFMPEG_CORRESPONDING_SOURCE_URL }} + FFMPEG_CORRESPONDING_SOURCE_SHA256: ${{ vars.OPENSTUDIO_FFMPEG_CORRESPONDING_SOURCE_SHA256 }} AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_STANDALONE_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG || '20260325' }} AI_RUNTIME_STANDALONE_PYTHON_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION || '3.10.20' }} AI_RUNTIME_STANDALONE_FLAVOR: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR || 'install_only' }} RELEASE_SITE_URL: https://openstudio.org.in - DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} - WINDOWS_CODESIGN_CERT_BASE64: ${{ secrets.WINDOWS_CODESIGN_CERT_BASE64 }} - WINDOWS_CODESIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_CERT_PASSWORD }} - WINDOWS_CODESIGN_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CODESIGN_CERT_THUMBPRINT }} - WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }} steps: - - uses: actions/checkout@v5 - - - name: Install Doppler CLI - if: env.DOPPLER_TOKEN != '' - uses: dopplerhq/cli-action@v3 - - - name: Load release secrets from Doppler - if: env.DOPPLER_TOKEN != '' - shell: pwsh - run: | - $secrets = doppler secrets download --no-file --format json | ConvertFrom-Json -AsHashtable - foreach ($entry in $secrets.GetEnumerator()) { - if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($entry.Key))) { - continue - } - - "$($entry.Key)=$($entry.Value)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - } + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize version shell: pwsh @@ -60,9 +44,9 @@ jobs: $aiRuntimeVersion = "${env:AI_RUNTIME_VERSION}" -replace '^v', '' "AI_RUNTIME_VERSION=$aiRuntimeVersion" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -71,7 +55,9 @@ jobs: run: | cd frontend npm ci + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } npm run build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Install Inno Setup shell: pwsh @@ -85,20 +71,67 @@ jobs: shell: pwsh run: ./tools/setup-asio-sdk.ps1 -Destination $env:ASIO_SDK_DIR - - name: Install optional ONNX Runtime - if: vars.OPENSTUDIO_SETUP_ONNXRUNTIME == 'true' + - name: Install ONNX Runtime + shell: pwsh + run: ./tools/setup-onnxruntime.ps1 -Version $env:ONNXRUNTIME_VERSION -ExpectedSha256 $env:ONNXRUNTIME_WIN_X64_SHA256 + + - name: Detect Doppler fallback shell: pwsh - run: ./tools/setup-onnxruntime.ps1 -Version $env:ONNXRUNTIME_VERSION + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + run: | + $configured = if ([string]::IsNullOrWhiteSpace($env:DOPPLER_TOKEN)) { "false" } else { "true" } + "DOPPLER_CONFIGURED=$configured" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + + - name: Install Doppler CLI + if: env.DOPPLER_CONFIGURED == 'true' + uses: dopplerhq/cli-action@014df23b1329b615816a38eb5f473bb9000700b1 # v3 - name: Configure CMake shell: pwsh + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + TONE3000_PUBLISHABLE_KEY: ${{ vars.TONE3000_PUBLISHABLE_KEY != '' && vars.TONE3000_PUBLISHABLE_KEY || secrets.TONE3000_PUBLISHABLE_KEY }} + OPENSTUDIO_TONE3000_CLIENT_ID_VALUE: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID_VALUE }} + OPENSTUDIO_TONE3000_CLIENT_ID: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID }} run: | + function Get-AllowlistedDopplerValue { + param([Parameter(Mandatory = $true)][string]$Name) + if ([string]::IsNullOrWhiteSpace($env:DOPPLER_TOKEN)) { return "" } + $value = (& doppler secrets get $Name --plain 2>$null | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { return "" } + if (-not [string]::IsNullOrWhiteSpace($value)) { Write-Host "::add-mask::$value" } + return $value + } + + $tone3000ClientId = $env:TONE3000_PUBLISHABLE_KEY + if ([string]::IsNullOrWhiteSpace($tone3000ClientId)) { + $tone3000ClientId = $env:OPENSTUDIO_TONE3000_CLIENT_ID_VALUE + } + if ([string]::IsNullOrWhiteSpace($tone3000ClientId)) { + $tone3000ClientId = $env:OPENSTUDIO_TONE3000_CLIENT_ID + } + if ([string]::IsNullOrWhiteSpace($tone3000ClientId)) { + foreach ($name in @("TONE3000_PUBLISHABLE_KEY", "OPENSTUDIO_TONE3000_CLIENT_ID_VALUE", "OPENSTUDIO_TONE3000_CLIENT_ID")) { + $tone3000ClientId = Get-AllowlistedDopplerValue -Name $name + if (-not [string]::IsNullOrWhiteSpace($tone3000ClientId)) { break } + } + } + if ([string]::IsNullOrWhiteSpace($tone3000ClientId)) { + throw "A TONE3000 publishable OAuth client ID is required for release builds." + } + $env:DOPPLER_TOKEN = "" + $env:TONE3000_PUBLISHABLE_KEY = "" + $env:OPENSTUDIO_TONE3000_CLIENT_ID_VALUE = "" + $env:OPENSTUDIO_TONE3000_CLIENT_ID = "" + cmake -S . -B $env:BUILD_DIR -A x64 ` "-DOPENSTUDIO_APP_VERSION=$env:VERSION" ` "-DOPENSTUDIO_UPDATE_MANIFEST_URL_VALUE=$env:RELEASE_SITE_URL/releases/stable/latest.json" ` "-DOPENSTUDIO_UPDATE_APPCAST_URL_VALUE=$env:RELEASE_SITE_URL/appcast/windows-stable.xml" ` "-DOPENSTUDIO_RELEASES_PAGE_URL_VALUE=$env:RELEASE_SITE_URL/download" ` "-DOPENSTUDIO_UPDATE_CHANNEL_VALUE=stable" ` + "-DOPENSTUDIO_TONE3000_CLIENT_ID_VALUE=$tone3000ClientId" ` "-DJUCE_ASIOSDK_PATH=$env:GITHUB_WORKSPACE/$env:ASIO_SDK_DIR" ` "-DOPENSTUDIO_REQUIRE_ASIO=ON" ` "-DOPENSTUDIO_ENABLE_EXTERNAL_PYTHON_AI_FALLBACK=OFF" ` @@ -112,21 +145,61 @@ jobs: shell: pwsh run: ./tools/validate-runtime-bundle.ps1 -Platform windows -BundlePath "$env:BUILD_DIR/OpenStudio_artefacts/Release" -ExpectedVersion $env:VERSION -EnforceLeanBundle + - name: Run Windows native-window lifecycle release gate + shell: pwsh + run: | + $exePath = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.exe" + $report = Join-Path $env:RUNNER_TEMP "OpenStudio_WindowLifecycleHarness.json" + ./tools/run-window-lifecycle-smoke.ps1 -AppPath $exePath -ReportPath $report -TimeoutSeconds 180 + - name: Prepare Windows signing certificate shell: pwsh + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + WINDOWS_CODESIGN_CERT_BASE64: ${{ secrets.WINDOWS_CODESIGN_CERT_BASE64 }} run: | - if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_CERT_BASE64)) { + $certificateBase64 = $env:WINDOWS_CODESIGN_CERT_BASE64 + if ([string]::IsNullOrWhiteSpace($certificateBase64) -and -not [string]::IsNullOrWhiteSpace($env:DOPPLER_TOKEN)) { + $certificateBase64 = (& doppler secrets get WINDOWS_CODESIGN_CERT_BASE64 --plain 2>$null | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { $certificateBase64 = "" } + } + $env:DOPPLER_TOKEN = "" + $env:WINDOWS_CODESIGN_CERT_BASE64 = "" + if ([string]::IsNullOrWhiteSpace($certificateBase64)) { Write-Host "No Windows code-signing certificate secret was provided." exit 0 } + Write-Host "::add-mask::$certificateBase64" $certPath = Join-Path $env:RUNNER_TEMP "openstudio-codesign.pfx" - [IO.File]::WriteAllBytes($certPath, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_CERT_BASE64)) + [IO.File]::WriteAllBytes($certPath, [Convert]::FromBase64String($certificateBase64)) "WINDOWS_CODESIGN_CERT_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Package Windows installer shell: pwsh + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + WINDOWS_CODESIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_CERT_PASSWORD }} + WINDOWS_CODESIGN_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CODESIGN_CERT_THUMBPRINT }} + WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }} run: | + function Get-ReleaseValue { + param([Parameter(Mandatory = $true)][string]$Name, [string]$DirectValue) + if (-not [string]::IsNullOrWhiteSpace($DirectValue)) { return $DirectValue } + if ([string]::IsNullOrWhiteSpace($env:DOPPLER_TOKEN)) { return "" } + $value = (& doppler secrets get $Name --plain 2>$null | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { return "" } + if (-not [string]::IsNullOrWhiteSpace($value)) { Write-Host "::add-mask::$value" } + return $value + } + + $certificatePassword = Get-ReleaseValue -Name "WINDOWS_CODESIGN_CERT_PASSWORD" -DirectValue $env:WINDOWS_CODESIGN_CERT_PASSWORD + $certificateThumbprint = Get-ReleaseValue -Name "WINDOWS_CODESIGN_CERT_THUMBPRINT" -DirectValue $env:WINDOWS_CODESIGN_CERT_THUMBPRINT + $timestampUrl = Get-ReleaseValue -Name "WINDOWS_TIMESTAMP_URL" -DirectValue $env:WINDOWS_TIMESTAMP_URL + $env:DOPPLER_TOKEN = "" + $env:WINDOWS_CODESIGN_CERT_PASSWORD = "" + $env:WINDOWS_CODESIGN_CERT_THUMBPRINT = "" + $env:WINDOWS_TIMESTAMP_URL = "" $arguments = @{ Version = $env:VERSION SourceDir = "$env:BUILD_DIR/OpenStudio_artefacts/Release" @@ -136,16 +209,16 @@ jobs: $arguments.CertificateFile = $env:WINDOWS_CODESIGN_CERT_PATH } - if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_CERT_PASSWORD)) { - $arguments.CertificatePassword = $env:WINDOWS_CODESIGN_CERT_PASSWORD + if (-not [string]::IsNullOrWhiteSpace($certificatePassword)) { + $arguments.CertificatePassword = $certificatePassword } - if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_CERT_THUMBPRINT)) { - $arguments.CertificateThumbprint = $env:WINDOWS_CODESIGN_CERT_THUMBPRINT + if (-not [string]::IsNullOrWhiteSpace($certificateThumbprint)) { + $arguments.CertificateThumbprint = $certificateThumbprint } - if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_TIMESTAMP_URL)) { - $arguments.TimestampUrl = $env:WINDOWS_TIMESTAMP_URL + if (-not [string]::IsNullOrWhiteSpace($timestampUrl)) { + $arguments.TimestampUrl = $timestampUrl } ./tools/package-windows-release.ps1 @arguments @@ -164,10 +237,34 @@ jobs: } } - - uses: actions/upload-artifact@v4 + - name: Stage checksum-pinned FFmpeg corresponding source + shell: pwsh + run: | + $sourceUrl = ([string]$env:FFMPEG_CORRESPONDING_SOURCE_URL).Trim() + $expectedSha256 = ([string]$env:FFMPEG_CORRESPONDING_SOURCE_SHA256).Trim().ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($sourceUrl) -or + $expectedSha256 -notmatch '^[0-9a-f]{64}$') { + throw "Set OPENSTUDIO_FFMPEG_CORRESPONDING_SOURCE_URL and OPENSTUDIO_FFMPEG_CORRESPONDING_SOURCE_SHA256 to the complete corresponding-source archive for the exact bundled FFmpeg 8.0.1 static build." + } + $sourceUri = [Uri]$sourceUrl + if (-not $sourceUri.IsAbsoluteUri -or $sourceUri.Scheme -ne "https") { + throw "OPENSTUDIO_FFMPEG_CORRESPONDING_SOURCE_URL must be an absolute HTTPS URL." + } + + $sourcePath = "dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip" + Invoke-WebRequest -Uri $sourceUri -OutFile $sourcePath -UseBasicParsing + $actualSha256 = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualSha256 -ne $expectedSha256) { + Remove-Item -LiteralPath $sourcePath -Force + throw "FFmpeg corresponding-source checksum mismatch. Expected '$expectedSha256' but found '$actualSha256'." + } + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-release - path: dist/windows/OpenStudio-Setup-x64.exe + path: | + dist/windows/OpenStudio-Setup-x64.exe + dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip if-no-files-found: error build-macos: @@ -180,33 +277,8 @@ jobs: AI_RUNTIME_STANDALONE_PYTHON_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_PYTHON_VERSION || '3.10.20' }} AI_RUNTIME_STANDALONE_FLAVOR: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_FLAVOR || 'install_only' }} RELEASE_SITE_URL: https://openstudio.org.in - DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} - MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} - MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} steps: - - uses: actions/checkout@v5 - - - name: Install Doppler CLI - if: env.DOPPLER_TOKEN != '' - uses: dopplerhq/cli-action@v3 - - - name: Load release secrets from Doppler - if: env.DOPPLER_TOKEN != '' - shell: pwsh - run: | - $secrets = doppler secrets download --no-file --format json | ConvertFrom-Json -AsHashtable - foreach ($entry in $secrets.GetEnumerator()) { - if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($entry.Key))) { - continue - } - - "$($entry.Key)=$($entry.Value)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - } + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize version shell: bash @@ -214,9 +286,9 @@ jobs: echo "VERSION=${VERSION#v}" >> "$GITHUB_ENV" echo "AI_RUNTIME_VERSION=${AI_RUNTIME_VERSION#v}" >> "$GITHUB_ENV" - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -227,32 +299,103 @@ jobs: npm ci npm run build + - name: Detect Doppler fallback + shell: bash + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + run: | + if [[ -n "${DOPPLER_TOKEN:-}" ]]; then + echo "DOPPLER_CONFIGURED=true" >> "$GITHUB_ENV" + else + echo "DOPPLER_CONFIGURED=false" >> "$GITHUB_ENV" + fi + + - name: Install Doppler CLI + if: env.DOPPLER_CONFIGURED == 'true' + uses: dopplerhq/cli-action@014df23b1329b615816a38eb5f473bb9000700b1 # v3 + - name: Prepare macOS signing certificate - if: env.MACOS_CERTIFICATE_BASE64 != '' shell: bash + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} run: | + resolve_release_value() { + local name="$1" + local direct_value="${2:-}" + if [[ -n "$direct_value" ]]; then printf '%s' "$direct_value"; return 0; fi + if [[ -z "${DOPPLER_TOKEN:-}" ]]; then return 0; fi + doppler secrets get "$name" --plain 2>/dev/null || true + } + + CERTIFICATE_BASE64="$(resolve_release_value MACOS_CERTIFICATE_BASE64 "${MACOS_CERTIFICATE_BASE64:-}")" + CERTIFICATE_PASSWORD="$(resolve_release_value MACOS_CERTIFICATE_PASSWORD "${MACOS_CERTIFICATE_PASSWORD:-}")" + KEYCHAIN_PASSWORD="$(resolve_release_value MACOS_KEYCHAIN_PASSWORD "${MACOS_KEYCHAIN_PASSWORD:-}")" + unset DOPPLER_TOKEN MACOS_CERTIFICATE_BASE64 MACOS_CERTIFICATE_PASSWORD MACOS_KEYCHAIN_PASSWORD + if [[ -z "$CERTIFICATE_BASE64" ]]; then + echo "No macOS code-signing certificate was provided." + exit 0 + fi + if [[ -z "$CERTIFICATE_PASSWORD" || -z "$KEYCHAIN_PASSWORD" ]]; then + echo "A macOS certificate requires both certificate and keychain passwords." >&2 + exit 1 + fi + echo "::add-mask::$CERTIFICATE_BASE64" + echo "::add-mask::$CERTIFICATE_PASSWORD" + echo "::add-mask::$KEYCHAIN_PASSWORD" + CERT_PATH="$RUNNER_TEMP/openstudio-macos-signing.p12" KEYCHAIN_PATH="$RUNNER_TEMP/openstudio-signing.keychain-db" EXISTING_KEYCHAINS="$(security list-keychains -d user | sed 's/^[[:space:]]*//; s/^\"//; s/\"$//')" - echo "$MACOS_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" - security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + echo "$CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/xcrun - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/xcrun + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security list-keychains -d user -s "$KEYCHAIN_PATH" $EXISTING_KEYCHAINS echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" - name: Configure CMake shell: bash + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + TONE3000_PUBLISHABLE_KEY: ${{ vars.TONE3000_PUBLISHABLE_KEY != '' && vars.TONE3000_PUBLISHABLE_KEY || secrets.TONE3000_PUBLISHABLE_KEY }} + OPENSTUDIO_TONE3000_CLIENT_ID_VALUE: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID_VALUE }} + OPENSTUDIO_TONE3000_CLIENT_ID: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID }} run: | + resolve_doppler_value() { + local name="$1" + if [[ -z "${DOPPLER_TOKEN:-}" ]]; then return 0; fi + doppler secrets get "$name" --plain 2>/dev/null || true + } + + TONE3000_CLIENT_ID="${TONE3000_PUBLISHABLE_KEY:-${OPENSTUDIO_TONE3000_CLIENT_ID_VALUE:-${OPENSTUDIO_TONE3000_CLIENT_ID:-}}}" + if [[ -z "$TONE3000_CLIENT_ID" ]]; then + for name in TONE3000_PUBLISHABLE_KEY OPENSTUDIO_TONE3000_CLIENT_ID_VALUE OPENSTUDIO_TONE3000_CLIENT_ID; do + TONE3000_CLIENT_ID="$(resolve_doppler_value "$name")" + if [[ -n "$TONE3000_CLIENT_ID" ]]; then + echo "::add-mask::$TONE3000_CLIENT_ID" + break + fi + done + fi + if [ -z "$TONE3000_CLIENT_ID" ]; then + echo "A TONE3000 publishable OAuth client ID is required for release builds." >&2 + exit 1 + fi + unset DOPPLER_TOKEN TONE3000_PUBLISHABLE_KEY OPENSTUDIO_TONE3000_CLIENT_ID_VALUE OPENSTUDIO_TONE3000_CLIENT_ID + cmake -S . -B "$BUILD_DIR" \ -DOPENSTUDIO_APP_VERSION="$VERSION" \ -DOPENSTUDIO_UPDATE_MANIFEST_URL_VALUE="$RELEASE_SITE_URL/releases/stable/latest.json" \ -DOPENSTUDIO_UPDATE_APPCAST_URL_VALUE="$RELEASE_SITE_URL/appcast/macos-stable.xml" \ -DOPENSTUDIO_RELEASES_PAGE_URL_VALUE="$RELEASE_SITE_URL/download" \ -DOPENSTUDIO_UPDATE_CHANNEL_VALUE="stable" \ + -DOPENSTUDIO_TONE3000_CLIENT_ID_VALUE="$TONE3000_CLIENT_ID" \ -DOPENSTUDIO_ENABLE_EXTERNAL_PYTHON_AI_FALLBACK=OFF \ -DFETCHCONTENT_UPDATES_DISCONNECTED=ON @@ -270,9 +413,41 @@ jobs: ./tools/validate-runtime-bundle.ps1 -Platform macos -BundlePath $appPath.FullName -ExpectedVersion $env:VERSION -EnforceLeanBundle + - name: Validate universal macOS executable + shell: bash + run: | + APP_PATH="$(find "$BUILD_DIR" -type d -name 'OpenStudio.app' | head -n 1)" + ARCHS="$(lipo -archs "$APP_PATH/Contents/MacOS/OpenStudio")" + echo "OpenStudio architectures: $ARCHS" + [[ " $ARCHS " == *" arm64 "* ]] + [[ " $ARCHS " == *" x86_64 "* ]] + - name: Package macOS DMG shell: bash + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | + resolve_release_value() { + local name="$1" + local direct_value="${2:-}" + if [[ -n "$direct_value" ]]; then printf '%s' "$direct_value"; return 0; fi + if [[ -z "${DOPPLER_TOKEN:-}" ]]; then return 0; fi + doppler secrets get "$name" --plain 2>/dev/null || true + } + + export MACOS_CODESIGN_IDENTITY="$(resolve_release_value MACOS_CODESIGN_IDENTITY "${MACOS_CODESIGN_IDENTITY:-}")" + export APPLE_ID="$(resolve_release_value APPLE_ID "${APPLE_ID:-}")" + export APPLE_TEAM_ID="$(resolve_release_value APPLE_TEAM_ID "${APPLE_TEAM_ID:-}")" + export APPLE_APP_PASSWORD="$(resolve_release_value APPLE_APP_PASSWORD "${APPLE_APP_PASSWORD:-}")" + for value in "$MACOS_CODESIGN_IDENTITY" "$APPLE_ID" "$APPLE_TEAM_ID" "$APPLE_APP_PASSWORD"; do + if [[ -n "$value" ]]; then echo "::add-mask::$value"; fi + done + unset DOPPLER_TOKEN + chmod +x ./tools/package-macos-release.sh APP_PATH="$(find "$BUILD_DIR" -type d -name 'OpenStudio.app' | head -n 1)" if [[ -z "$APP_PATH" ]]; then @@ -280,6 +455,40 @@ jobs: exit 1 fi ./tools/package-macos-release.sh "$APP_PATH" "$VERSION" + if [[ -n "$MACOS_CODESIGN_IDENTITY" ]]; then + echo "OPENSTUDIO_MACOS_PACKAGE_SIGNED=true" >> "$GITHUB_ENV" + fi + if [[ -n "$APPLE_ID" && -n "$APPLE_TEAM_ID" && -n "$APPLE_APP_PASSWORD" ]]; then + echo "OPENSTUDIO_MACOS_PACKAGE_NOTARIZED=true" >> "$GITHUB_ENV" + fi + + - name: Run packaged macOS native-window lifecycle release gate + shell: bash + run: | + MOUNT_DIR="$(mktemp -d)" + REPORT_PATH="$RUNNER_TEMP/OpenStudio_PackagedWindowLifecycleHarness.json" + cleanup() { + hdiutil detach "$MOUNT_DIR" -quiet || true + rmdir "$MOUNT_DIR" 2>/dev/null || true + } + trap cleanup EXIT + + hdiutil attach "dist/macos/OpenStudio-macOS.dmg" -nobrowse -readonly -mountpoint "$MOUNT_DIR" + APP_PATH="$MOUNT_DIR/OpenStudio.app" + test -x "$APP_PATH/Contents/MacOS/OpenStudio" + + if [[ "${OPENSTUDIO_MACOS_PACKAGE_SIGNED:-false}" == "true" ]]; then + codesign --verify --deep --strict --verbose=2 "$APP_PATH" + fi + + if [[ "${OPENSTUDIO_MACOS_PACKAGE_NOTARIZED:-false}" == "true" ]]; then + spctl --assess --type execute --verbose=2 "$APP_PATH" + fi + + pwsh -NoProfile -File ./tools/run-window-lifecycle-smoke.ps1 \ + -AppPath "$APP_PATH/Contents/MacOS/OpenStudio" \ + -ReportPath "$REPORT_PATH" \ + -TimeoutSeconds 180 - name: Verify macOS release outputs shell: bash @@ -291,7 +500,7 @@ jobs: shell: bash run: security delete-keychain "$KEYCHAIN_PATH" || true - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: macos-release path: dist/macos/OpenStudio-macOS.dmg @@ -303,9 +512,10 @@ jobs: VERSION: ${{ github.event.inputs.version || github.ref_name }} BUILD_DIR: build-release-linux ONNXRUNTIME_VERSION: 1.24.4 + ONNXRUNTIME_LINUX_X64_SHA256: 3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747 RELEASE_SITE_URL: https://openstudio.org.in steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize version shell: bash @@ -319,13 +529,13 @@ jobs: libasound2-dev libjack-jackd2-dev \ libwebkit2gtk-4.1-dev libgtk-3-dev \ libgl1-mesa-dev libfreetype6-dev libfontconfig1-dev \ - libcurl4-openssl-dev \ + libcurl4-openssl-dev libsecret-tools \ libx11-dev libxext-dev libxrandr-dev libxi-dev \ libxinerama-dev libxcursor-dev libxcomposite-dev - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -336,11 +546,54 @@ jobs: run: | URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz" wget -q "$URL" -O onnxruntime.tgz + echo "${ONNXRUNTIME_LINUX_X64_SHA256} onnxruntime.tgz" | sha256sum --check --strict mkdir -p thirdparty/onnxruntime tar -xzf onnxruntime.tgz --strip-components=1 -C thirdparty/onnxruntime + - name: Detect Doppler fallback + shell: bash + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + run: | + if [[ -n "${DOPPLER_TOKEN:-}" ]]; then + echo "DOPPLER_CONFIGURED=true" >> "$GITHUB_ENV" + else + echo "DOPPLER_CONFIGURED=false" >> "$GITHUB_ENV" + fi + + - name: Install Doppler CLI + if: env.DOPPLER_CONFIGURED == 'true' + uses: dopplerhq/cli-action@014df23b1329b615816a38eb5f473bb9000700b1 # v3 + - name: Configure CMake + env: + DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} + TONE3000_PUBLISHABLE_KEY: ${{ vars.TONE3000_PUBLISHABLE_KEY != '' && vars.TONE3000_PUBLISHABLE_KEY || secrets.TONE3000_PUBLISHABLE_KEY }} + OPENSTUDIO_TONE3000_CLIENT_ID_VALUE: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID_VALUE }} + OPENSTUDIO_TONE3000_CLIENT_ID: ${{ secrets.OPENSTUDIO_TONE3000_CLIENT_ID }} run: | + resolve_doppler_value() { + local name="$1" + if [[ -z "${DOPPLER_TOKEN:-}" ]]; then return 0; fi + doppler secrets get "$name" --plain 2>/dev/null || true + } + + TONE3000_CLIENT_ID="${TONE3000_PUBLISHABLE_KEY:-${OPENSTUDIO_TONE3000_CLIENT_ID_VALUE:-${OPENSTUDIO_TONE3000_CLIENT_ID:-}}}" + if [[ -z "$TONE3000_CLIENT_ID" ]]; then + for name in TONE3000_PUBLISHABLE_KEY OPENSTUDIO_TONE3000_CLIENT_ID_VALUE OPENSTUDIO_TONE3000_CLIENT_ID; do + TONE3000_CLIENT_ID="$(resolve_doppler_value "$name")" + if [[ -n "$TONE3000_CLIENT_ID" ]]; then + echo "::add-mask::$TONE3000_CLIENT_ID" + break + fi + done + fi + if [ -z "$TONE3000_CLIENT_ID" ]; then + echo "A TONE3000 publishable OAuth client ID is required for release builds." >&2 + exit 1 + fi + unset DOPPLER_TOKEN TONE3000_PUBLISHABLE_KEY OPENSTUDIO_TONE3000_CLIENT_ID_VALUE OPENSTUDIO_TONE3000_CLIENT_ID + cmake -S . -B "$BUILD_DIR" -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DOPENSTUDIO_APP_VERSION="$VERSION" \ @@ -348,12 +601,17 @@ jobs: -DOPENSTUDIO_UPDATE_APPCAST_URL_VALUE="$RELEASE_SITE_URL/appcast/linux-stable.xml" \ -DOPENSTUDIO_RELEASES_PAGE_URL_VALUE="$RELEASE_SITE_URL/download" \ -DOPENSTUDIO_UPDATE_CHANNEL_VALUE="stable" \ + -DOPENSTUDIO_TONE3000_CLIENT_ID_VALUE="$TONE3000_CLIENT_ID" \ -DOPENSTUDIO_ENABLE_EXTERNAL_PYTHON_AI_FALLBACK=OFF \ -DFETCHCONTENT_UPDATES_DISCONNECTED=ON - name: Build OpenStudio run: cmake --build "$BUILD_DIR" --config Release --target OpenStudio + - name: Validate Linux runtime bundle + shell: pwsh + run: ./tools/validate-runtime-bundle.ps1 -Platform linux -BundlePath "$env:BUILD_DIR/OpenStudio_artefacts/Release" -ExpectedVersion $env:VERSION -EnforceLeanBundle + - name: Package AppImage run: | chmod +x ./tools/package-linux-release.sh @@ -362,7 +620,25 @@ jobs: - name: Verify Linux release outputs run: test -f "dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" - - uses: actions/upload-artifact@v4 + - name: Validate packaged Linux AppImage contents + shell: bash + run: | + APPIMAGE="$GITHUB_WORKSPACE/dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" + EXTRACT_ROOT="$(mktemp -d)" + cleanup() { rm -rf -- "$EXTRACT_ROOT"; } + trap cleanup EXIT + chmod +x "$APPIMAGE" + ( + cd "$EXTRACT_ROOT" + "$APPIMAGE" --appimage-extract >/dev/null + ) + pwsh -NoProfile -File "$GITHUB_WORKSPACE/tools/validate-runtime-bundle.ps1" \ + -Platform linux \ + -BundlePath "$EXTRACT_ROOT/squashfs-root/usr/bin" \ + -ExpectedVersion "$VERSION" \ + -EnforceLeanBundle + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: linux-release path: dist/linux/OpenStudio-*-linux-x86_64.AppImage @@ -381,28 +657,10 @@ jobs: RELEASE_NOTES_FILE: ${{ github.event.inputs.release_notes_file || 'packaging/release-notes-template.md' }} AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_RELEASE_TAG }} - DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} WEBSITE_REPO: ${{ vars.OPENSTUDIO_WEBSITE_REPO != '' && vars.OPENSTUDIO_WEBSITE_REPO || 'sdevil7th/OpenStudioWebsite' }} WEBSITE_DISPATCH_EVENT_TYPE: ${{ vars.OPENSTUDIO_WEBSITE_DISPATCH_EVENT_TYPE != '' && vars.OPENSTUDIO_WEBSITE_DISPATCH_EVENT_TYPE || 'openstudio_release_published' }} steps: - - uses: actions/checkout@v5 - - - name: Install Doppler CLI - if: env.DOPPLER_TOKEN != '' - uses: dopplerhq/cli-action@v3 - - - name: Load release secrets from Doppler - if: env.DOPPLER_TOKEN != '' - shell: pwsh - run: | - $secrets = doppler secrets download --no-file --format json | ConvertFrom-Json -AsHashtable - foreach ($entry in $secrets.GetEnumerator()) { - if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($entry.Key))) { - continue - } - - "$($entry.Key)=$($entry.Value)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - } + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Normalize version shell: bash @@ -430,17 +688,17 @@ jobs: echo "Either run .github/workflows/ai-runtime-release.yml first, or point OPENSTUDIO_AI_RUNTIME_RELEASE_TAG / OPENSTUDIO_AI_RUNTIME_VERSION at an existing runtime release." >&2 exit 1 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: windows-release path: dist/windows - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: macos-release path: dist/macos - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: linux-release path: dist/linux @@ -469,6 +727,7 @@ jobs: run: | $requiredFiles = @( "dist/windows/OpenStudio-Setup-x64.exe", + "dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip", "dist/macos/OpenStudio-macOS.dmg", "dist/ai-runtime/OpenStudio-AI-Runtime-windows-base-x64.zip", "dist/ai-runtime/OpenStudio-AI-Runtime-macos-arm64.zip", @@ -508,6 +767,7 @@ jobs: -WindowsInstallerArguments "/SP- /NOICONS" ` -MacAssetPath "dist/macos/OpenStudio-macOS.dmg" ` -MacAssetUrl $macosUrl ` + -MacMinimumSystemVersion "12.0" ` -LinuxAssetPath "dist/linux/OpenStudio-$env:VERSION-linux-x86_64.AppImage" ` -LinuxAssetUrl $linuxUrl ` -WindowsBaseAiRuntimeAssetPath "dist/ai-runtime/OpenStudio-AI-Runtime-windows-base-x64.zip" ` @@ -546,16 +806,25 @@ jobs: -MetadataDir "dist/release-metadata" ` -OutputDir "dist/release-publish-assets" + - name: Add FFmpeg source archive to release checksums + shell: pwsh + run: | + $sourcePath = "dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip" + $checksumsPath = "dist/release-publish-assets/OpenStudio-checksums.txt" + $sourceHash = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash.ToLowerInvariant() + "$sourceHash OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip" | + Add-Content -LiteralPath $checksumsPath -Encoding utf8 + - name: Publish GitHub release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', github.event.inputs.version) }} name: OpenStudio ${{ env.VERSION }} body_path: ${{ env.RELEASE_NOTES_FILE }} fail_on_unmatched_files: true - overwrite_files: true files: | dist/windows/OpenStudio-Setup-x64.exe + dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip dist/macos/OpenStudio-macOS.dmg dist/linux/OpenStudio-*-linux-x86_64.AppImage dist/release-publish-assets/OpenStudio-checksums.txt @@ -578,7 +847,7 @@ jobs: fi - name: Trigger website release publish - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3 with: token: ${{ secrets.OPENSTUDIO_WEBSITE_DISPATCH_TOKEN }} repository: ${{ env.WEBSITE_REPO }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5d27263..337d200 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,19 +7,55 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + jobs: + verify-input-profiles: + name: Input profiles (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-14] + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version-file: frontend/.nvmrc + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: Run frontend unit tests + run: npm test + + - name: Install Playwright Chromium + run: npx playwright install chromium + + - name: Run shortcut and input-profile browser tests + run: npm run test:e2e + verify-windows: runs-on: windows-latest env: VERSION: 0.0.0 BUILD_DIR: build-verify-windows ASIO_SDK_DIR: thirdparty/asio + ONNXRUNTIME_VERSION: 1.24.4 + ONNXRUNTIME_WIN_X64_SHA256: d2319fddfb6ea4db99ccc4b60c85c517bcd855721f5daa6a06d40d7cb2ee2357 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -28,12 +64,18 @@ jobs: run: | cd frontend npm ci + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } npm run build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Install ASIO SDK shell: pwsh run: ./tools/setup-asio-sdk.ps1 -Destination $env:ASIO_SDK_DIR + - name: Install ONNX Runtime + shell: pwsh + run: ./tools/setup-onnxruntime.ps1 -Version $env:ONNXRUNTIME_VERSION -ExpectedSha256 $env:ONNXRUNTIME_WIN_X64_SHA256 + - name: Install Windows prerequisite installers shell: pwsh run: | @@ -112,17 +154,101 @@ jobs: throw "Windows startup self-test failed." } + - name: Regress WebView2 preflight from a non-writable install directory + shell: pwsh + run: | + $sourceBundle = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release" + $protectedBundle = Join-Path $env:RUNNER_TEMP "OpenStudio-Protected-Install" + Copy-Item -LiteralPath $sourceBundle -Destination $protectedBundle -Recurse + + # Model Program Files precisely: the test identity may read and + # execute the bundle but may not write beside OpenStudio.exe. + $acl = New-Object System.Security.AccessControl.DirectorySecurity + $acl.SetAccessRuleProtection($true, $false) + $inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit' + $none = [System.Security.AccessControl.PropagationFlags]::None + $allow = [System.Security.AccessControl.AccessControlType]::Allow + $deny = [System.Security.AccessControl.AccessControlType]::Deny + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name + # GitHub-hosted Windows runners execute as an administrator. An explicit + # user deny keeps the probe non-writable while the Administrators group + # retains the delete and ACL rights needed for runner cleanup. + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, 'Write', $inheritance, $none, $deny))) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, 'ReadAndExecute, Synchronize', $inheritance, $none, $allow))) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( + 'NT AUTHORITY\SYSTEM', 'FullControl', $inheritance, $none, $allow))) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( + 'BUILTIN\Administrators', 'FullControl', $inheritance, $none, $allow))) + Set-Acl -LiteralPath $protectedBundle -AclObject $acl + + $writeProbe = Join-Path $protectedBundle 'must-not-write.tmp' + $writeDenied = $false + try { + [System.IO.File]::WriteAllText($writeProbe, 'probe') + } catch [System.UnauthorizedAccessException] { + $writeDenied = $true + } + if ((Test-Path -LiteralPath $writeProbe) -or -not $writeDenied) { + throw "Staged install directory remained writable." + } + + $report = Join-Path $env:RUNNER_TEMP "OpenStudio_ProtectedInstallStartupSelfTest.txt" + $exePath = Join-Path $protectedBundle "OpenStudio.exe" + $process = Start-Process -FilePath $exePath -ArgumentList @("--startup-self-test", "--report", "`"$report`"") -Wait -PassThru + if ($process.ExitCode -ne 0) { + if (Test-Path -LiteralPath $report) { + Get-Content -LiteralPath $report + } + throw "WebView2 preflight failed from a non-writable install directory." + } + + $reportText = [System.IO.File]::ReadAllText($report) + $userDataMatch = [regex]::Match($reportText, '(?m)^webView2UserDataPath=(.+)$') + if (-not $userDataMatch.Success) { + throw "Protected-install self-test did not report its WebView2 user-data folder." + } + $userDataPath = [System.IO.Path]::GetFullPath($userDataMatch.Groups[1].Value.Trim()) + $protectedRoot = [System.IO.Path]::GetFullPath($protectedBundle).TrimEnd('\') + '\' + if ($userDataPath.StartsWith($protectedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "WebView2 user data incorrectly resolves inside the protected install tree: $userDataPath" + } + + - name: Run Windows native-window lifecycle smoke test + shell: pwsh + run: | + $exePath = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.exe" + $report = Join-Path $env:RUNNER_TEMP "OpenStudio_WindowLifecycleHarness.json" + ./tools/run-window-lifecycle-smoke.ps1 -AppPath $exePath -ReportPath $report -TimeoutSeconds 180 + + - name: Run NAM Rack deterministic regression + shell: pwsh + run: | + $exePath = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.exe" + ./tools/run-nam-rack-headless-regression.ps1 ` + -AppPath $exePath ` + -OutputRoot $env:RUNNER_TEMP ` + -Label "ci-release" ` + -TimeoutSeconds 360 ` + -SkipBuild + verify-macos: - runs-on: macos-14 + name: macOS native windows (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15-intel] env: VERSION: 0.0.0 BUILD_DIR: build-verify-macos steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -159,6 +285,14 @@ jobs: shell: pwsh run: ./tools/validate-runtime-bundle.ps1 -Platform macos -BundlePath "$env:APP_BUNDLE" -ExpectedVersion "$env:VERSION" -EnforceLeanBundle + - name: Validate universal macOS executable + shell: bash + run: | + ARCHS="$(lipo -archs "$APP_BUNDLE/Contents/MacOS/OpenStudio")" + echo "OpenStudio architectures: $ARCHS" + [[ " $ARCHS " == *" arm64 "* ]] + [[ " $ARCHS " == *" x86_64 "* ]] + - name: Run macOS startup self-test shell: bash run: | @@ -171,14 +305,22 @@ jobs: exit 1 fi + - name: Run macOS native-window lifecycle smoke test + shell: pwsh + run: | + $executable = Join-Path $env:APP_BUNDLE "Contents/MacOS/OpenStudio" + $report = Join-Path $env:RUNNER_TEMP "OpenStudio_WindowLifecycleHarness.json" + ./tools/run-window-lifecycle-smoke.ps1 -AppPath $executable -ReportPath $report -TimeoutSeconds 180 + verify-linux: runs-on: ubuntu-24.04 env: VERSION: 0.0.0 BUILD_DIR: build-verify-linux ONNXRUNTIME_VERSION: 1.24.4 + ONNXRUNTIME_LINUX_X64_SHA256: 3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Install system dependencies shell: bash @@ -186,9 +328,9 @@ jobs: bash ./tools/setup-linux-prereqs.sh sudo apt-get install -y xvfb - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 20 + node-version-file: frontend/.nvmrc cache: npm cache-dependency-path: frontend/package-lock.json @@ -204,6 +346,7 @@ jobs: run: | URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz" wget -q "$URL" -O onnxruntime.tgz + echo "${ONNXRUNTIME_LINUX_X64_SHA256} onnxruntime.tgz" | sha256sum --check --strict mkdir -p thirdparty/onnxruntime tar -xzf onnxruntime.tgz --strip-components=1 -C thirdparty/onnxruntime @@ -249,3 +392,21 @@ jobs: - name: Verify Linux AppImage output shell: bash run: test -f "dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" + + - name: Validate packaged Linux AppImage contents + shell: bash + run: | + APPIMAGE="$GITHUB_WORKSPACE/dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" + EXTRACT_ROOT="$(mktemp -d)" + cleanup() { rm -rf -- "$EXTRACT_ROOT"; } + trap cleanup EXIT + chmod +x "$APPIMAGE" + ( + cd "$EXTRACT_ROOT" + "$APPIMAGE" --appimage-extract >/dev/null + ) + pwsh -NoProfile -File "$GITHUB_WORKSPACE/tools/validate-runtime-bundle.ps1" \ + -Platform linux \ + -BundlePath "$EXTRACT_ROOT/squashfs-root/usr/bin" \ + -ExpectedVersion "$VERSION" \ + -EnforceLeanBundle diff --git a/.gitignore b/.gitignore index 6a1618c..f7c2a39 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ cmake_install.cmake # IDE .vs .vscode +.claude/settings.local.json *.sln *.vcxproj *.vcxproj.filters @@ -52,9 +53,28 @@ get-pip.py /tools/linuxdeploy-*.AppImage # Local pitch/render regression outputs +/qa/ +/.playwright-cli/ +/frontend/.playwright-cli/ +/.codex-key-diff.txt +/.codex_tmp/ +/output/ +/tmp_nam_rack_runs/ +/tmp_clean_guitar_runs/ +/docs/nam-rack-design-html/ +/docs/nam-rack-neural-clean-handoff/ +/docs/nam_visual_design/ +/docs/nam_visual_qa/ /tmp_pitch_runs/ /pitch_debug_captures/ /*_signal_chain_debug/ + +# Local maintainer planning (not for the public repository) +/docs/roadmap.internal.md + +# NAM design masters are archived outside the repo; runtime WebP assets stay tracked. +/frontend/src/assets/nam/design/**/*.png +/frontend/src/assets/nam/rack-studio-backdrop-v2.png /signal_chain_debug/ /tests/fixtures/pitch-regression/runs/ /tests/fixtures/pitch-regression/**/*.wav @@ -135,15 +155,14 @@ get-pip.py # ML models (large binary files, downloaded at setup/runtime) /resources/models/*.onnx +!/resources/models/basic_pitch_nmp.onnx /resources/models/*.ckpt /resources/models/*.bin /resources/models/*.ort # Audio test samples /audio_test_samples/ - -# Local QA screenshots/reports generated by browser and native harnesses -/qa/ +/resources/test_fixtures/ # Logs *.log diff --git a/AGENTS.md b/AGENTS.md index a93a64d..2b9495b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Studio13-v3 +# OpenStudio A hybrid DAW (Digital Audio Workstation) with a **JUCE C++ backend** for audio processing and a **React/TypeScript frontend** rendered in WebView2. @@ -26,7 +26,7 @@ C++ (JUCE) Backend React/TypeScript Frontend ## Directory Structure ``` -Studio13-v3/ +OpenStudio/ ├── Source/ # C++ backend │ ├── Main.cpp # JUCE app entry point │ ├── MainComponent.h/cpp # Hosts WebBrowserComponent + AudioEngine, exposes native functions to JS @@ -138,9 +138,9 @@ Studio13-v3/ ├── tools/ # ffmpeg.exe, stem_separator.py, setup scripts ├── resources/ # ONNX models, presets, resources ├── build/ # CMake build output -├── CMakeLists.txt # C++ build: JUCE 8.0.0, ASIO SDK, WebView2, VST3, ONNX Runtime +├── CMakeLists.txt # C++ build: JUCE 9.0.1, ASIO SDK, WebView2, VST3, ONNX Runtime ├── build.py # Python orchestrator: cmake + npm + vite dev server -├── pitch_corrector_feat_plan.md # Detailed pitch editor implementation plan (Melodyne/RePitch/VariAudio parity) +├── docs/roadmap.md # Open release/product work; completed implementation history stays in Git └── WORKFLOWS.md # Dev workflow docs ``` @@ -177,7 +177,7 @@ Before asking for manual testing: - Make sure the latest frontend code is built into `frontend/dist` when packaged fallback could be used. - Run `cmake --build build --config Debug` after frontend or C++ changes so the Debug app and copied `webui` assets are current. - Do not require the user to pre-run Vite, npm, or any other server. `python build.py dev --run` must start what it needs. -- Stop any Codex-started dev servers, harness browsers, or background Vite/npm processes before handing off. Verify port `5173` is not left occupied by a Codex-started process. +- Stop any Codex-started dev servers, harness browsers, or background Vite/npm processes before handing off. Verify port `5183` is not left occupied by a Codex-started process. - In the handoff, state that the CMake Debug build was completed and that no pre-running server is required. ## Key Technical Details @@ -232,7 +232,7 @@ For **continuous edits** (faders, knobs), use the begin/commit pattern: `beginXE ### Pitch Editor Subsystem -The pitch editor enables vocal pitch correction with both real-time (auto-tune style) and graphical (Melodyne-style) modes. The implementation plan for reaching Melodyne/RePitch/VariAudio quality is in `pitch_corrector_feat_plan.md`. +The pitch editor enables vocal pitch correction with both real-time (auto-tune style) and graphical (Melodyne-style) modes. Current open work and release decisions live in `docs/roadmap.md`; renderer evidence is retained only in the dedicated pitch research notes. **Architecture**: ``` @@ -310,6 +310,23 @@ Real-time corrector: - Semantic colors: `daw-record` (red), `daw-mute` (green), `daw-solo` (yellow), `daw-fx` (lime) - UI components in `components/ui/` use variant pattern (default, primary, success, danger, etc.) +### Frontend Styling and Visual QA + +- Do not generate stylesheet strings at runtime, mount JSX `
" << safeDetail << "