diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 1676d716..74dfc038 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -40,6 +40,11 @@ jobs: with: fetch-depth: 0 + - name: Configure native capture display + run: | + swift scripts/native-ui/hosted-display.swift --configure-hosted + swift scripts/native-ui/hosted-display.swift --check + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/docs/design/DOCUMENTATION-MEDIA-CONTRACT.md b/docs/design/DOCUMENTATION-MEDIA-CONTRACT.md index da618f0b..205690aa 100644 --- a/docs/design/DOCUMENTATION-MEDIA-CONTRACT.md +++ b/docs/design/DOCUMENTATION-MEDIA-CONTRACT.md @@ -41,3 +41,5 @@ Permitted post-build changes are regular, non-executable README/documentation Ma The optional `focusedBoardAssets` capture list permits only `board-5000.png`, at 1360×900 native content pixels. It has the same build, app digest, timestamp, byte-hash and committed-file requirements as the maintained captures. A published focused image requires its original capture entry. Run `node --import tsx ../scripts/native-ui/board-performance.mjs ` from `server/`. After that feature run passes, supply its `report.json` as the final argument to `pnpm docs:capture-media capture `. The capture command verifies the same candidate identity and original screenshot hash before incorporating the focused image. This supplements the fourteen maintained assets and ten task-mode views; it does not replace them. + +The signed workflow configures its disposable macOS runner to an available, unscaled 1920×1080 display mode before building. The setting lasts only for that runner login session. `swift scripts/native-ui/hosted-display.swift --check` is read-only and verifies the display prerequisite locally; `--configure-hosted` is restricted to GitHub Actions. Native acceptance records the actual display work area and fails before the matrix if it cannot contain the retained capture sizes. diff --git a/scripts/native-ui/contract.mjs b/scripts/native-ui/contract.mjs index 3c4f6917..bf5ddb8d 100644 --- a/scripts/native-ui/contract.mjs +++ b/scripts/native-ui/contract.mjs @@ -36,8 +36,8 @@ export const settingsSections = [ 'Maintenance', ]; export const contentSizes = { - // Hosted macOS runners expose only 760px of usable content height. Keep the - // expanded width while using a height the native window manager can honor. + // Retain the original matched capture sizes. The hosted workflow configures + // a 1920x1080 display before packaging; default runner displays can be smaller. normal: { width: 1700, height: 760 }, minimum: { width: 1180, height: 760 }, }; diff --git a/scripts/native-ui/hosted-display.swift b/scripts/native-ui/hosted-display.swift new file mode 100644 index 00000000..7a25da2d --- /dev/null +++ b/scripts/native-ui/hosted-display.swift @@ -0,0 +1,47 @@ +import CoreGraphics +import Foundation + +func fail(_ message: String) -> Never { + FileHandle.standardError.write(Data((message + "\n").utf8)) + exit(1) +} + +let argument = CommandLine.arguments.dropFirst().first ?? "--check" +guard ["--check", "--configure-hosted"].contains(argument) else { + fail("Usage: swift scripts/native-ui/hosted-display.swift [--check|--configure-hosted]") +} +let display = CGMainDisplayID() +if argument == "--configure-hosted" { + guard ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" else { + fail("Display configuration is restricted to the disposable GitHub Actions runner") + } + let modes = CGDisplayCopyAllDisplayModes(display, nil) as? [CGDisplayMode] ?? [] + guard let mode = modes.first(where: { + $0.width == 1920 && $0.height == 1080 && $0.pixelWidth == 1920 && $0.pixelHeight == 1080 + }) else { + fail("Runner has no unscaled 1920x1080 mode. Available: " + modes.map { + "\($0.width)x\($0.height) (\($0.pixelWidth)x\($0.pixelHeight) pixels)" + }.joined(separator: ", ")) + } + var configuration: CGDisplayConfigRef? + guard CGBeginDisplayConfiguration(&configuration) == .success, let configuration else { + fail("Cannot begin runner display configuration") + } + guard CGConfigureDisplayWithDisplayMode(configuration, display, mode, nil) == .success else { + CGCancelDisplayConfiguration(configuration) + fail("Cannot select runner display mode") + } + guard CGCompleteDisplayConfiguration(configuration, .forSession) == .success else { + fail("Cannot apply runner display mode for this login session") + } +} +guard let actual = CGDisplayCopyDisplayMode(display) else { fail("No active display mode") } +let evidence: [String: Any] = [ + "width": actual.width, "height": actual.height, + "pixelWidth": actual.pixelWidth, "pixelHeight": actual.pixelHeight, + "configured": argument == "--configure-hosted" +] +print(String(data: try JSONSerialization.data(withJSONObject: evidence, options: [.sortedKeys]), encoding: .utf8)!) +guard actual.width >= 1920 && actual.height >= 1080 else { + fail("Native capture requires at least 1920x1080 logical display points") +} diff --git a/scripts/native-ui/run.mjs b/scripts/native-ui/run.mjs index b44c7185..44788beb 100644 --- a/scripts/native-ui/run.mjs +++ b/scripts/native-ui/run.mjs @@ -685,6 +685,15 @@ async function checkSeededRendererFailures() { } try { await launch(); + report.display = await app.evaluate(({ screen }) => { + const { bounds, workArea, scaleFactor } = screen.getPrimaryDisplay(); + return { bounds, workArea, scaleFactor }; + }); + await persist(); + assert( + report.display.workArea.width >= 1780 && report.display.workArea.height >= 960, + 'Native capture needs a work area of at least 1780x960; configure the runner display first' + ); report.titlebarAction = await verifyConfiguredTitlebarAction(app, page); report.menuCommands = await verifyNativeMenuCommands(app, page); report.settingsWindow = await verifyNativeSettingsWindow(app, page);