name: release # Pushing a v* tag builds Frame Control for macOS, Windows and Linux and attaches # the installers to that tag's GitHub release (created as a draft if missing). # Pull requests that touch the app build the same installers as artifacts. on: push: tags: ["v*"] pull_request: paths: ["app/**", "ui/**", "scripts/**", "frame/**", "apk-catalog/**", ".github/workflows/release.yml"] workflow_dispatch: permissions: contents: write jobs: # One job makes the draft, before the builds: three matrix jobs each running # "view || create" could race and make duplicate drafts. The draft is tied to # the pushed tag (the tag must already exist on GitHub, then tag_name is set # explicitly), so scripts/publish-release.sh and `gh release upload` find it # by tag. It is created with the REST API so the id comes straight back: the # release list can lag a fresh draft (v0.4.2's first run found nothing and # PATCHed releases/ with an empty id). tests/test_release_workflow.py runs # this step against tests/fakegh. draft: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Create the draft release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | tag="${GITHUB_REF_NAME}" # A rerun finds the draft an earlier attempt made. id=$(gh api --paginate "repos/$GH_REPO/releases?per_page=100" --jq ".[] | select(.tag_name == \"$tag\") | .id" | head -n 1) if [ -z "$id" ]; then # Given a missing tag, GitHub would create it on the default branch. gh api "repos/$GH_REPO/git/ref/tags/$tag" >/dev/null \ || { echo "tag $tag isn't on GitHub" >&2; exit 1; } id=$(gh api -X POST "repos/$GH_REPO/releases" -f tag_name="$tag" -f name="Frame Control ${tag#v}" \ -F draft=true -f body="" --jq .id) fi [ -n "$id" ] || { echo "no release id for $tag; not PATCHing releases/" >&2; exit 1; } gh api -X PATCH "repos/$GH_REPO/releases/$id" -f tag_name="$tag" --jq '"release " + (.id|tostring) + " tag_name " + .tag_name' build: needs: draft # Still runs for pull requests and manual runs, where the draft job is skipped. if: ${{ !failure() && !cancelled() }} strategy: fail-fast: false matrix: include: - os: macos-latest script: dist files: app/dist/*.dmg app/dist/*.zip - os: windows-latest script: dist:win files: app/dist/*.exe app/dist/*.zip - os: ubuntu-latest script: dist:linux files: app/dist/*.AppImage app/dist/*.deb runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "24" - name: Build working-directory: app shell: bash run: npm ci && npm run ${{ matrix.script }} env: CSC_IDENTITY_AUTO_DISCOVERY: "false" # The installer, not just the unpacked build: its 7-Zip payload once dropped # the OpenXR layer's arm64 library without any error. Install it silently # and compare every installed file with the unpacked build. - name: Check the Windows installer installs every file if: matrix.os == 'windows-latest' shell: pwsh run: | $setup = Get-ChildItem app/dist/Frame-Control-Setup-*.exe | Select-Object -First 1 $p = Start-Process -FilePath $setup.FullName -ArgumentList '/S' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "installer exited $($p.ExitCode)" } $installed = Join-Path $env:LOCALAPPDATA 'Programs\Frame Control' for ($i = 0; $i -lt 60 -and -not (Test-Path (Join-Path $installed 'Uninstall Frame Control.exe')); $i++) { Start-Sleep 2 } node app/build/check-resources.js (Join-Path $installed 'resources') app/dist/win-unpacked/resources if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Upload to the release if: startsWith(github.ref, 'refs/tags/') shell: bash env: GH_TOKEN: ${{ github.token }} run: | gh release upload "${GITHUB_REF_NAME}" ${{ matrix.files }} --clobber - uses: actions/upload-artifact@v4 with: name: frame-control-${{ matrix.os }} path: | app/dist/*.dmg app/dist/*.exe app/dist/*.zip app/dist/*.AppImage app/dist/*.deb if-no-files-found: ignore