Skip to content

Commit 81bd042

Browse files
Merge branch 'main' into fix/130-broken-e2e-testids
2 parents 5551b74 + 0effa7b commit 81bd042

234 files changed

Lines changed: 16310 additions & 2094 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/launch.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
"runtimeExecutable": "npm",
2727
"runtimeArgs": ["--prefix", "website", "run", "serve", "--", "--port", "3111"],
2828
"port": 3111
29+
},
30+
{
31+
"name": "docs-dev",
32+
"runtimeExecutable": "npm",
33+
"runtimeArgs": ["--prefix", "website", "run", "dev", "--", "--port", "3112"],
34+
"port": 3112
2935
}
3036
]
3137
}

.github/workflows/build.yml

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,42 @@ jobs:
224224
VERSION="$(node -e "console.log(require('./package.json').version)")"
225225
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
226226
227+
# `--${{ matrix.arch }}` above does NOT restrict the architecture: the
228+
# `arch` list in electron-builder.json5's `mac.target` names both x64 and
229+
# arm64 and the config wins, so BOTH bundles are produced in every job —
230+
# x64 in release/<ver>/mac/, arm64 in release/<ver>/mac-arm64/. The old
231+
# `find release/<ver> ... | head -n1` took whichever came first in
232+
# directory order (x64, in practice), so the arm64 job could package the
233+
# x64 bundle into a DMG named `-arm64-`. Nothing downstream compared the
234+
# name against the contents, so that would have published silently.
227235
- name: Find .app bundle
228236
id: find_app
229237
run: |
230238
VERSION="${{ steps.version.outputs.version }}"
231-
APP_BUNDLE="$(find "release/${VERSION}" -maxdepth 4 -name "*.app" -type d | head -n1)"
239+
if [[ "${{ matrix.arch }}" == "arm64" ]]; then ARCH_DIR="mac-arm64"; else ARCH_DIR="mac"; fi
240+
APP_BUNDLE="$(find "release/${VERSION}/${ARCH_DIR}" -maxdepth 2 -name "*.app" -type d | head -n1)"
232241
if [[ -z "$APP_BUNDLE" ]]; then
233-
echo "::error::No .app bundle found in release/${VERSION}/"
242+
echo "::error::No .app bundle found in release/${VERSION}/${ARCH_DIR}/"
234243
find "release/${VERSION}" -maxdepth 4 -print || true
235244
exit 1
236245
fi
237246
echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT"
238247
248+
# The guard for the above: refuse to build a DMG whose name would not
249+
# match its contents. An Intel bundle on an Apple Silicon Mac runs under
250+
# Rosetta 2 — compositor, encoder and whisper all translated — which is
251+
# slow enough to be unusable, so a mislabelled DMG is a real user harm.
252+
- name: Verify .app architecture matches the job
253+
run: |
254+
BIN="${{ steps.find_app.outputs.app_bundle }}/Contents/MacOS/Openscreen"
255+
if [[ "${{ matrix.arch }}" == "arm64" ]]; then EXPECTED="arm64"; else EXPECTED="x86_64"; fi
256+
ACTUAL="$(lipo -archs "$BIN")"
257+
echo "job arch=${{ matrix.arch }} expected=${EXPECTED} actual=${ACTUAL}"
258+
if [[ " ${ACTUAL} " != *" ${EXPECTED} "* ]]; then
259+
echo "::error::The ${{ matrix.arch }} job produced a '${ACTUAL}' bundle — refusing to publish a mislabelled DMG"
260+
exit 1
261+
fi
262+
239263
- name: Verify .app code signature
240264
if: steps.signing.outputs.enabled == 'true'
241265
run: codesign --verify --deep --strict "${{ steps.find_app.outputs.app_bundle }}"
@@ -245,7 +269,17 @@ jobs:
245269
run: |
246270
VERSION="${{ steps.version.outputs.version }}"
247271
ARCH="${{ matrix.arch }}"
248-
DMG_NAME="Openscreen-Mac-${ARCH}-${VERSION}.dmg"
272+
# Name the DMG after the machine, not the instruction set. "x64" reads
273+
# to most people as "the normal 64-bit one" and "arm64" as the exotic
274+
# variant, which is exactly backwards on any Mac sold since 2020 — and
275+
# picking the wrong one silently costs Rosetta 2. `Intel` and
276+
# `Apple-Silicon` are what About This Mac shows the user.
277+
case "$ARCH" in
278+
arm64) ARCH_LABEL="Apple-Silicon" ;;
279+
x64) ARCH_LABEL="Intel" ;;
280+
*) ARCH_LABEL="$ARCH" ;;
281+
esac
282+
DMG_NAME="Openscreen-macOS-${ARCH_LABEL}-${VERSION}.dmg"
249283
RELEASE_DIR="release/${VERSION}"
250284
DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}"
251285
STAGING="${RELEASE_DIR}/dmg-staging"
@@ -473,3 +507,64 @@ jobs:
473507
--latest \
474508
--title "$TAG"
475509
fi
510+
511+
- name: Refresh the docs /download page
512+
# Only a stable release changes what /releases/latest resolves to, so a
513+
# pre-release would rebuild the site to byte-identical output.
514+
#
515+
# Dispatched against main on purpose: the github-pages environment only
516+
# permits `main` to deploy, so docs.yml's old `on: release` trigger ran
517+
# with a tag ref and failed its deploy every time. See docs.yml.
518+
if: ${{ steps.release.outputs.is_prerelease == 'false' }}
519+
timeout-minutes: 20
520+
env:
521+
GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }}
522+
run: |
523+
latest_dispatch() {
524+
gh run list \
525+
--repo "$GITHUB_REPOSITORY" \
526+
--workflow docs.yml \
527+
--event workflow_dispatch \
528+
--branch main \
529+
--limit 1 \
530+
--json databaseId \
531+
--jq '.[0].databaseId // empty'
532+
}
533+
534+
# `gh workflow run` prints nothing we can key off, so remember which
535+
# dispatch was newest beforehand and wait for a different one to appear.
536+
PREVIOUS_RUN_ID="$(latest_dispatch)"
537+
gh workflow run docs.yml --ref main --repo "$GITHUB_REPOSITORY"
538+
539+
RUN_ID=""
540+
for _ in $(seq 1 30); do
541+
sleep 5
542+
CANDIDATE="$(latest_dispatch)"
543+
if [[ -n "$CANDIDATE" && "$CANDIDATE" != "$PREVIOUS_RUN_ID" ]]; then
544+
RUN_ID="$CANDIDATE"
545+
break
546+
fi
547+
done
548+
549+
if [[ -z "$RUN_ID" ]]; then
550+
echo "::error::Dispatched docs.yml but no new run appeared within 150s"
551+
exit 1
552+
fi
553+
554+
gh run watch "$RUN_ID" --repo "$GITHUB_REPOSITORY" --interval 15 || true
555+
556+
CONCLUSION="$(gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --json conclusion --jq '.conclusion')"
557+
case "$CONCLUSION" in
558+
success)
559+
echo "Docs rebuilt and deployed by run $RUN_ID"
560+
;;
561+
cancelled)
562+
# docs.yml cancels in-flight runs sharing a ref, so a push to main
563+
# landing right now replaces this rebuild with a newer one.
564+
echo "::warning::Docs run $RUN_ID was cancelled, most likely superseded by a newer main run"
565+
;;
566+
*)
567+
echo "::error::Docs run $RUN_ID concluded '$CONCLUSION' - /download may still list the previous release"
568+
exit 1
569+
;;
570+
esac

.github/workflows/bump-nix-package.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ on:
1010
required: true
1111
type: string
1212

13+
# GITHUB_TOKEN only pushes the branch here — the PR itself is opened with the
14+
# PAT below, so no `pull-requests: write` is needed (and it never worked).
1315
permissions:
1416
contents: write
15-
pull-requests: write
1617

1718
jobs:
1819
bump:
@@ -78,7 +79,13 @@ jobs:
7879
7980
- name: Create PR
8081
env:
81-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82+
# NOT GITHUB_TOKEN: the repo has "Allow GitHub Actions to create and
83+
# approve pull requests" turned off, so `gh pr create` dies with
84+
# "GitHub Actions is not permitted to create or approve pull requests"
85+
# — it did exactly that on v1.7.0, after pushing the branch, and #136
86+
# had to be opened by hand. The PAT every other release workflow
87+
# already uses has no such restriction, and its PRs trigger CI.
88+
GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }}
8289
VERSION: ${{ steps.meta.outputs.version }}
8390
HASH: ${{ steps.hash.outputs.hash }}
8491
BRANCH: ${{ steps.meta.outputs.branch }}
@@ -112,7 +119,5 @@ jobs:
112119
- \`npmDepsHash\` → \`${HASH}\` (computed via \`prefetch-npm-deps package-lock.json\`)
113120
114121
Merge this so Nix users (NixOS, Home Manager, \`nix run github:${{ github.repository }}\`) pick up the new release.
115-
116-
> Note: PRs opened by \`GITHUB_TOKEN\` don't auto-trigger CI. The diff is two lines — review the change here, then merge. If you want CI to run, push an empty commit to this branch or close-and-reopen the PR.
117122
EOF
118123
)"

.github/workflows/ci.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,80 @@ jobs:
190190
cd crates
191191
cargo check -p openscreen-compositor -p compositor-view-napi --all-targets
192192
193+
# Le troisieme cote, et le dernier angle mort : le Rust Linux n'etait compile
194+
# NULLE PART en CI. Les deux jobs ci-dessus couvrent macOS (test) et Windows
195+
# (check) ; `compositor_linux.rs`, `pipeline_linux.rs`, `d3d_linux.rs` et les
196+
# 2154 lignes du moteur wgpu ne passaient que par le poste des contributeurs.
197+
#
198+
# Ce que le trou cachait, trouve en ouvrant ce job : `export_timing.rs` et
199+
# `output_geometry_golden.rs` ne compilaient pas sous Linux — ils appellent
200+
# `probe_frame_count` / `readback_resized`, qui n'existent que cote Windows et
201+
# macOS. Les fichiers de `tests/` etant compiles quelle que soit la plateforme,
202+
# le crate entier etait incompilable en `--tests` sur Linux, en silence.
203+
#
204+
# `cargo test` et pas `check` : `mesa-vulkan-drivers` donne au runner un ICD
205+
# Vulkan logiciel (lavapipe), donc `cpu_backend_linux.rs` exerce POUR DE VRAI le
206+
# backend CPU, qui est la propriete que cette PR ajoute. Un runner GitHub n'ayant
207+
# pas de GPU, c'est meme le seul endroit ou ce chemin est teste sans forcage.
208+
rust-linux-compositor-check:
209+
name: Rust test (Linux compositor)
210+
runs-on: ubuntu-latest
211+
steps:
212+
- uses: actions/checkout@v4
213+
# Pas de ./.github/actions/setup : `fetch-ffmpeg.mjs` n'importe que des
214+
# builtins node, donc `npm ci` serait une minute d'installation pour rien.
215+
- uses: actions/setup-node@v4
216+
with:
217+
node-version-file: .nvmrc
218+
# libclang-dev, pas libclang1 : bindgen a besoin de libclang pour lire les
219+
# headers ffmpeg, et c'est le paquet -dev qui apporte AUSSI les headers
220+
# built-in de clang. Sans eux bindgen echoue sur `stddef.h file not found`.
221+
# mesa-vulkan-drivers : l'ICD lavapipe. Sans lui le runner n'a aucun
222+
# adaptateur Vulkan et le backend CPU serait intestable. C'est le meme
223+
# paquet que le .deb declare desormais en dependance (electron-builder.json5).
224+
- name: Install libclang and the Mesa Vulkan drivers
225+
run: |
226+
sudo apt-get update
227+
sudo apt-get install -y --no-install-recommends libclang-dev mesa-vulkan-drivers
228+
- name: Vendor the pinned ffmpeg SDK
229+
run: npm run fetch:ffmpeg:sdk
230+
# `crates/.cargo/config.toml` pose FFMPEG_DIR (arbre win64) et LIBCLANG_PATH
231+
# (chemin Windows) dans un `[env]` GLOBAL — cargo n'a pas de
232+
# `[target.<cfg>.env]`. Les deux valeurs sont donc TOUJOURS renseignees et
233+
# fausses ici ; il faut les surcharger par de vraies variables
234+
# d'environnement, qui gagnent (`force = false` par defaut).
235+
- name: Resolve the toolchain paths
236+
run: |
237+
echo "FFMPEG_DIR=$GITHUB_WORKSPACE/crates/thirdparty/ffmpeg-linux64-lgpl-shared" >> "$GITHUB_ENV"
238+
# `sort -V | tail -1` et pas `find | head -1` : l'image du runner embarque
239+
# plusieurs LLVM, et l'ordre de parcours du systeme de fichiers n'est pas
240+
# trie -- on pouvait donc tomber sur une version differente de celle
241+
# qu'apt vient d'installer. Les headers built-in de clang etant lies a la
242+
# version de libclang, le symptome aurait ete `stddef.h file not found`,
243+
# qui ne designe pas sa cause. On prend la plus recente, deterministe.
244+
libclang=$(ls -1 /usr/lib/llvm-*/lib/libclang.so 2>/dev/null | sort -V | tail -1)
245+
# Echouer ici plutot que de laisser bindgen partir sur le chemin Windows
246+
# et rendre une erreur qui ne designe pas la cause non plus.
247+
test -n "$libclang" || { echo "libclang introuvable apres l'installation"; exit 1; }
248+
echo "LIBCLANG_PATH=$(dirname "$libclang")" >> "$GITHUB_ENV"
249+
- name: cargo test (compositor)
250+
env:
251+
# Les .so ffmpeg vendorises ne sont dans aucun chemin systeme : sans ca
252+
# le binaire de test se lance puis meurt sur `libavformat.so.62`.
253+
LD_LIBRARY_PATH: ${{ github.workspace }}/crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib
254+
# Fait ECHOUER `cpu_backend_linux.rs` s'il n'obtient pas le backend CPU,
255+
# au lieu de le sauter en silence comme sur un poste sans lavapipe.
256+
OPENSCREEN_REQUIRE_CPU_BACKEND: "1"
257+
run: |
258+
cd crates
259+
cargo test -p openscreen-compositor --lib --tests
260+
- name: cargo build (napi addon)
261+
env:
262+
LD_LIBRARY_PATH: ${{ github.workspace }}/crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib
263+
run: |
264+
cd crates
265+
cargo build -p compositor-view-napi --release
266+
193267
semantic-pr:
194268
name: Validate PR title (semantic)
195269
runs-on: ubuntu-latest

.github/workflows/docs.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ on:
1212
- "website/**"
1313
- ".github/workflows/docs.yml"
1414
# The /download page resolves the current release's assets at build time so it
15-
# can link each platform to its actual file. Without this trigger that data
16-
# would freeze at whatever the last website/** change saw, and the page would
17-
# keep serving the previous version's binaries after every release.
18-
# Pre-releases are skipped: /releases/latest ignores them, so the built output
19-
# would be byte-identical.
20-
release:
21-
types: [published]
15+
# can link each platform to its actual file. Without a post-release rebuild that
16+
# data would freeze at whatever the last website/** change saw, and the page
17+
# would keep serving the previous version's binaries after every release.
18+
#
19+
# That rebuild is a `workflow_dispatch` fired by build.yml once the release is
20+
# published, NOT an `on: release` trigger. A release event runs with
21+
# github.ref = refs/tags/vX.Y.Z, and the github-pages environment only allows
22+
# `main` to deploy, so the deploy job failed on every stable release (it never
23+
# surfaced earlier because pre-releases skipped the build entirely). Dispatching
24+
# against main both satisfies that policy and publishes main's docs rather than
25+
# the release branch's older snapshot.
2226
workflow_dispatch:
2327

2428
# Cancel in-flight runs on the same ref so fast follow-up pushes
@@ -34,9 +38,6 @@ jobs:
3438
build:
3539
name: Build site
3640
runs-on: ubuntu-latest
37-
# A pre-release does not change what /releases/latest resolves to, so
38-
# rebuilding for one would burn a run to produce identical output.
39-
if: github.event_name != 'release' || github.event.release.prerelease == false
4041
steps:
4142
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
4243
with:
@@ -66,7 +67,7 @@ jobs:
6667
needs: build
6768
if: >-
6869
(github.event_name == 'push' && github.ref == 'refs/heads/main')
69-
|| github.event_name == 'release'
70+
|| (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
7071
environment:
7172
name: github-pages
7273
url: ${{ steps.deployment.outputs.page_url }}

.github/workflows/nix-check.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Nix
2+
3+
# nix/package.nix records `npmDepsHash`, a hash of the npm dependency set that
4+
# package-lock.json resolves to. The two have to agree or `nix build` fails
5+
# outright, and nothing here ever checked that they did.
6+
#
7+
# The only thing that refreshed the hash was bump-nix-package.yml, which fires
8+
# on stable releases. `src` is this repo's own tree, not a fetched tarball, so
9+
# every lockfile change landing between two releases left main's hash pointing
10+
# at dependencies that no longer existed. It last matched on 2026-07-05 (v1.6.0)
11+
# while package-lock.json moved ten more times, so `nix run github:…` was broken
12+
# for four weeks with nothing reporting it.
13+
#
14+
# Release-time bumps can't fix that — the drift starts the moment a lockfile PR
15+
# merges. This runs on the PR that causes it and prints the hash to paste.
16+
on:
17+
pull_request:
18+
paths:
19+
- package-lock.json
20+
- nix/**
21+
- .github/workflows/nix-check.yml
22+
# Same branch list as ci.yml, and for the reason its own comment gives: a
23+
# release branch is the last place to skip a check. PRs need no filter here —
24+
# the trigger above has none, so they are covered wherever they land — but a
25+
# direct push or a rebase force-push onto a long-lived branch is not a PR and
26+
# would otherwise go unchecked.
27+
push:
28+
branches: [main, feat/ai-edition, "release/**"]
29+
paths:
30+
- package-lock.json
31+
- nix/**
32+
- .github/workflows/nix-check.yml
33+
34+
# Read-only, and stated rather than inherited: the repo default happens to be
35+
# `read` today, which is exactly the kind of repo-level setting that silently
36+
# changed this workflow's sibling out from under it.
37+
permissions:
38+
contents: read
39+
40+
jobs:
41+
npm-deps-hash:
42+
name: npmDepsHash matches package-lock.json
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- uses: cachix/install-nix-action@v27
48+
with:
49+
nix_path: nixpkgs=channel:nixos-unstable
50+
extra_nix_config: |
51+
experimental-features = nix-command flakes
52+
53+
- name: Compare recorded hash against the lockfile
54+
run: |
55+
set -euo pipefail
56+
EXPECTED=$(nix run nixpkgs#prefetch-npm-deps -- package-lock.json)
57+
RECORDED=$(sed -nE 's|^[[:space:]]*npmDepsHash[[:space:]]*=[[:space:]]*"([^"]*)";|\1|p' nix/package.nix)
58+
59+
echo "recorded in nix/package.nix: ${RECORDED:-<none>}"
60+
echo "expected from package-lock.json: $EXPECTED"
61+
62+
if [[ -z "$EXPECTED" ]]; then
63+
echo "::error::prefetch-npm-deps returned an empty hash"
64+
exit 1
65+
fi
66+
67+
if [[ "$EXPECTED" != "$RECORDED" ]]; then
68+
echo "::error file=nix/package.nix::npmDepsHash is stale — set it to $EXPECTED"
69+
exit 1
70+
fi
71+
72+
echo "In sync."

0 commit comments

Comments
 (0)