ci: build the boot stub, sign the manifest, and produce an installer #334
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: build | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - dev | |
| - release/* | |
| - feat/app-hub | |
| workflow_dispatch: | |
| inputs: | |
| sign: | |
| description: Code-sign with SignPath (release builds only) | |
| type: boolean | |
| default: false | |
| jobs: | |
| build: | |
| name: build-${{ matrix.platform }} | |
| runs-on: ${{ matrix.os }} | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: windows | |
| os: windows-2022 | |
| - platform: macos | |
| os: macos-26 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.ref }} | |
| submodules: true | |
| - name: Prepare environment variables | |
| run: | | |
| echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" | |
| echo "APP_VERSION=$(node -p "require('./package.json').version")" >> "$GITHUB_ENV" | |
| # ─── Toolchains (shared) ──────────────────────────────────────────── | |
| # .NET 10 SDK for both Pengu.Windows (AOT) and Pengu.MacOS (workload). | |
| - name: Setup .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| run_install: | | |
| - recursive: true | |
| # ─── Toolchains (Windows) ─────────────────────────────────────────── | |
| # MSBuild + the VS 2022 C++ workload (preinstalled on windows-2022). | |
| # PublishAot relies on the C++ toolchain shipped with the workload. | |
| - name: Setup MSBuild | |
| if: runner.os == 'Windows' | |
| uses: microsoft/setup-msbuild@v2 | |
| # ─── Toolchains (macOS) ───────────────────────────────────────────── | |
| # Microsoft.macOS.Sdk.net10.0_26.4 requires Xcode 26.x. Select the | |
| # latest stable Xcode on the runner; the workload install fails loudly | |
| # if 26+ isn't available. | |
| - name: Select latest Xcode | |
| if: runner.os == 'macOS' | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Install .NET macOS workload | |
| if: runner.os == 'macOS' | |
| run: sudo dotnet workload install macos | |
| # ─── Build (shared) ───────────────────────────────────────────────── | |
| # Preload bundle is #include'd into core (renderer.cc embeds preload.g.h). | |
| # Build it before core so the header exists. | |
| - name: Build preload | |
| run: pnpm --filter @pengujs/preload build | |
| # ─── Build (Windows) ──────────────────────────────────────────────── | |
| # C++ core -> core/bin/core.dll (the vcxproj writes there directly). | |
| - name: Build core (windows) | |
| if: runner.os == 'Windows' | |
| run: msbuild.exe core/core.vcxproj -p:Configuration=Release -p:Platform=x64 -nologo -m | |
| # The IFEO boot stub. Shares bootstrap.cc with core but links nothing | |
| # else from it, so it stays a few tens of KB with no CEF surface. | |
| # Writes to boot/bin/ (OutDir is $(SolutionDir)bin\ and there is no .sln, | |
| # so SolutionDir resolves to the project directory — same as core). | |
| - name: Build boot stub (windows) | |
| if: runner.os == 'Windows' | |
| run: msbuild.exe boot/boot.vcxproj -p:Configuration=Release -p:Platform=x64 -nologo -m | |
| # AOT-publish the .NET 10 host. The csproj's BuildHubBundle target runs | |
| # `pnpm --filter @pengujs/hub build` and zips packages/hub/dist/ into | |
| # app.dat as part of the build, and the PublishAppDat target adds app.dat | |
| # to ResolvedFileToPublish — so a single dotnet publish produces | |
| # Pengu.exe + app.dat in the output dir. | |
| - name: Publish app/Pengu.Windows (AOT) | |
| if: runner.os == 'Windows' | |
| run: | | |
| dotnet publish app/Pengu.Windows \ | |
| -c Release \ | |
| -r win-x64 \ | |
| --output app-publish | |
| # Clean portable payload: extract the artifact archive anywhere, run Pengu.exe. | |
| # app.dat is embedded inside Pengu.exe via <EmbeddedResource> — no | |
| # separate file ships alongside the exe. | |
| - name: Stage portable payload (windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| mkdir -p out/ | |
| cp app-publish/Pengu.exe out/ | |
| cp app-publish/WebView2Loader.dll out/ | |
| cp core/bin/core.dll out/ | |
| cp boot/bin/pengu-boot.exe out/ | |
| # ─── Build (macOS) ────────────────────────────────────────────────── | |
| # C++ core -> core/bin/core.dylib (the makefile's LIB_OUT_PATH). | |
| - name: Build core (macos) | |
| if: runner.os == 'macOS' | |
| run: make -C core -j release | |
| # Publish the .NET 10 macOS host as AOT + universal (arm64 + x86_64). | |
| # The csproj's <RuntimeIdentifiers> drives multi-arch — no -r flag. | |
| # The macOS workload publishes each RID, lipo's the resulting native | |
| # binaries, and stages the universal Pengu binary plus app.dat inside | |
| # Pengu.app/Contents/{MacOS,Resources}/. Expect 30–60 minutes due to | |
| # AOT compilation + per-RID lipo on a CI runner. | |
| - name: Publish app/Pengu.MacOS (AOT, universal) | |
| if: runner.os == 'macOS' | |
| run: | | |
| dotnet publish app/Pengu.MacOS \ | |
| -c Release \ | |
| --output app-publish | |
| # Stage two deliverables for the macOS artifact: | |
| # - Pengu-<ver>.pkg — installer with the universal AOT app inside | |
| # - Pengu.app — universal AOT build (x86_64 + arm64, ~28 MB), | |
| # drag-to-Applications form | |
| # | |
| # The .pkg lands in --output (app-publish/). The universal .app | |
| # always lands at bin/Release/net10.0-macos/Pengu.app regardless of | |
| # --output — `--output` only stages the .pkg installer, not the | |
| # bundle. The bin/.../Pengu.app already has full <BundleResource> | |
| # staging (app-icon.icns, core.dylib), lipo'd Mach-O, and the | |
| # workload's codesign applied. The per-RID .apps under | |
| # bin/Release/.../{osx-arm64,osx-x64}/ are intermediate skeletons — | |
| # not used for distribution. | |
| - name: Stage macOS deliverables | |
| if: runner.os == 'macOS' | |
| run: | | |
| mkdir -p out/ | |
| cp app-publish/*.pkg out/ | |
| cp -R app/Pengu.MacOS/bin/Release/net10.0-macos/Pengu.app out/Pengu.app | |
| codesign --verify --verbose=2 out/Pengu.app | |
| # ─── Code signing (Windows, opt-in) ───────────────────────────────── | |
| # Off by default. Every run still produces a complete, installable | |
| # payload — signing only changes whether it is trusted by the boot's | |
| # Authenticode gate and by SmartScreen. | |
| # | |
| # Two rounds, because the installer embeds the binaries: sign the | |
| # binaries, build the installer from the signed ones, then sign the | |
| # installer. Same shape v1.1.6 used. | |
| - name: Upload unsigned payload for signing | |
| if: runner.os == 'Windows' && inputs.sign | |
| uses: actions/upload-artifact@v4 | |
| id: unsigned-payload | |
| with: | |
| name: unsigned-payload-${{ env.SHORT_SHA }} | |
| path: out/ | |
| if-no-files-found: error | |
| - name: Sign binaries | |
| if: runner.os == 'Windows' && inputs.sign | |
| uses: signpath/github-action-submit-signing-request@v1 | |
| with: | |
| api-token: ${{ secrets.SIGNPATH_API_TOKEN }} | |
| organization-id: ${{ secrets.SIGNPATH_ORG_ID }} | |
| project-slug: PenguLoader | |
| signing-policy-slug: release-signing | |
| artifact-configuration-slug: zipped-app | |
| github-artifact-id: ${{ steps.unsigned-payload.outputs.artifact-id }} | |
| wait-for-completion: true | |
| output-artifact-directory: signed | |
| # From here on, the payload is whichever directory holds the binaries we | |
| # intend to ship — signed when we signed, otherwise the build output. | |
| - name: Resolve payload directory (windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| if [ "${{ inputs.sign }}" = "true" ]; then | |
| echo "PAYLOAD=signed" >> "$GITHUB_ENV" | |
| else | |
| echo "PAYLOAD=out" >> "$GITHUB_ENV" | |
| fi | |
| # MUST come after signing. Authenticode rewrites the PE, so a manifest | |
| # generated before signing would name a hash that no longer exists and | |
| # the boot would refuse the very core it shipped with. | |
| - name: Sign release manifest (windows) | |
| if: runner.os == 'Windows' | |
| env: | |
| PENGU_MANIFEST_KEY: ${{ secrets.PENGU_MANIFEST_KEY }} | |
| shell: pwsh | |
| run: | | |
| if (-not $env:PENGU_MANIFEST_KEY) { | |
| Write-Warning "PENGU_MANIFEST_KEY is not set — shipping without a manifest." | |
| Write-Warning "The boot will refuse this build and League will launch without plugins." | |
| exit 0 | |
| } | |
| ./build/sign-manifest.ps1 -InstallDir "$env:PAYLOAD" -Version "${{ env.APP_VERSION }}+${{ env.SHORT_SHA }}" | |
| # The release gate. A manifest signed with a key the shipped boot does | |
| # not trust produces a client that launches without plugins and no | |
| # obvious cause, so fail the build rather than ship it. | |
| - name: Verify manifest against the shipped boot (windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| if (-not (Test-Path "$env:PAYLOAD/pengu.manifest")) { | |
| Write-Warning "no manifest to verify" | |
| exit 0 | |
| } | |
| $pub = (Select-String -Path boot/trust_key.h -Pattern '0x[0-9A-F]{2}' -AllMatches | | |
| ForEach-Object { $_.Matches.Value }) -join '' | |
| ./build/sign-manifest.ps1 -InstallDir "$env:PAYLOAD" -Verify -PublicKey $pub | |
| # ─── Installer (Windows) ──────────────────────────────────────────── | |
| - name: Build installer (windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| # Version-agnostic: runner images and dev machines carry different | |
| # Inno majors, and pinning one turns an image bump into a red build. | |
| $iscc = Get-ChildItem "$env:ProgramFiles\Inno Setup *","${env:ProgramFiles(x86)}\Inno Setup *" ` | |
| -Directory -ErrorAction SilentlyContinue | | |
| Sort-Object Name -Descending | | |
| ForEach-Object { Join-Path $_.FullName 'ISCC.exe' } | | |
| Where-Object { Test-Path $_ } | | |
| Select-Object -First 1 | |
| if (-not $iscc) { throw "ISCC.exe not found" } | |
| Write-Host "using $iscc" | |
| New-Item -ItemType Directory -Force -Path installer | Out-Null | |
| & $iscc "/DPayloadDir=$(Resolve-Path $env:PAYLOAD)" "/DOutputDirectory=$(Resolve-Path installer)" build\setup.iss | |
| - name: Upload unsigned installer for signing | |
| if: runner.os == 'Windows' && inputs.sign | |
| uses: actions/upload-artifact@v4 | |
| id: unsigned-installer | |
| with: | |
| name: unsigned-installer-${{ env.SHORT_SHA }} | |
| path: installer/ | |
| if-no-files-found: error | |
| - name: Sign installer | |
| if: runner.os == 'Windows' && inputs.sign | |
| uses: signpath/github-action-submit-signing-request@v1 | |
| with: | |
| api-token: ${{ secrets.SIGNPATH_API_TOKEN }} | |
| organization-id: ${{ secrets.SIGNPATH_ORG_ID }} | |
| project-slug: PenguLoader | |
| signing-policy-slug: release-signing | |
| artifact-configuration-slug: zipped-installer | |
| github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }} | |
| wait-for-completion: true | |
| output-artifact-directory: installer | |
| # Drop the intermediates so the run doesn't publish unsigned copies | |
| # alongside the signed ones. | |
| - name: Delete signing intermediates | |
| if: runner.os == 'Windows' && inputs.sign | |
| uses: geekyeggo/delete-artifact@v5 | |
| with: | |
| name: | | |
| unsigned-payload-${{ env.SHORT_SHA }} | |
| unsigned-installer-${{ env.SHORT_SHA }} | |
| # ─── Artifact upload (shared) ─────────────────────────────────────── | |
| - name: Upload portable artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pengu-v${{ env.APP_VERSION }}-${{ env.SHORT_SHA }}-${{ matrix.platform }} | |
| path: ${{ runner.os == 'Windows' && env.PAYLOAD || 'out' }}/ | |
| if-no-files-found: error | |
| compression-level: 9 | |
| - name: Upload installer artifact (windows) | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pengu-v${{ env.APP_VERSION }}-${{ env.SHORT_SHA }}-installer | |
| path: installer/ | |
| if-no-files-found: error | |
| compression-level: 9 |