Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
name: CI

# Every job below exists because something it checks reached `main` unnoticed.
# The repository had no continuous integration until 2026-08-02; the first
# Linux run of the existing suite found four defects, all merged directly to
# `main`. See docs/IMPLEMENTATION_LOG.md for that record.
#
# Linux is the product target (Raspberry Pi 5 and ordinary x86-64 Linux).
# Windows is the development host. Both are exercised because defects have been
# found that are invisible on the other: a clippy lint that only fires where
# `PathBuf` is 32 bytes, and a launcher layout collision that only appears
# under Linux font metrics.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CI: "true"

Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add a top-level least-privilege permissions block.

The workflow declares no permissions, so every job inherits the repository default token scope. No job needs write access. Set read-only at the top level.

🔒 Proposed fix
+permissions:
+  contents: read
+
 env:
   CI: "true"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
env:
CI: "true"
permissions:
contents: read
env:
CI: "true"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 24 - 26, Add a top-level permissions
block in the workflow alongside the global env configuration, setting the GitHub
Actions token scope to read-only for repository contents. Do not grant write
access or add job-specific permissions.

Source: Linters/SAST tools

jobs:
node:
name: node / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Disable credential persistence on every actions/checkout step.

actions/checkout writes the job token into .git/config by default. No job in this workflow performs a git operation that needs the token. Set persist-credentials: false on Lines 36, 90, 129, and 171.

🔒 Proposed fix
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 36-36: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 36-36: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 36, Update every actions/checkout step in
the workflow, including the steps near lines 36, 90, 129, and 171, to set
persist-credentials to false while preserving their existing checkout
configuration.

Source: Linters/SAST tools


- uses: actions/setup-node@v4
with:
node-version: 22

# `packageManager` in package.json pins the exact pnpm version.
- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: pnpm install --frozen-lockfile

# prepare:assets fetches the pinned pose model and typeface by exact
# SHA-256, so a substituted upstream artifact fails here.
- name: Prepare pinned assets and generated sources
run: |
pnpm prepare:assets
pnpm prepare:catalog
pnpm prepare:schemas
Comment on lines +51 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Set shell: bash on multi-line run steps.

On windows-latest, GitHub Actions runs run blocks with PowerShell Core by default. PowerShell Core does not stop the script when a native command returns a non-zero exit code. Only the exit code of the last command reaches the runner. If pnpm prepare:assets fails here, pnpm prepare:schemas still runs and the step reports success.

Add shell: bash to this step. The same applies to the native job "Show toolchain" step (Lines 93-97) and the e2e job "Prepare pinned assets and generated sources" step (Lines 141-145).

🔧 Proposed fix
       - name: Prepare pinned assets and generated sources
+        shell: bash
         run: |
           pnpm prepare:assets
           pnpm prepare:catalog
           pnpm prepare:schemas
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Prepare pinned assets and generated sources
run: |
pnpm prepare:assets
pnpm prepare:catalog
pnpm prepare:schemas
- name: Prepare pinned assets and generated sources
shell: bash
run: |
pnpm prepare:assets
pnpm prepare:catalog
pnpm prepare:schemas
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 51 - 55, Add shell: bash to the
multi-line “Prepare pinned assets and generated sources” run step, and apply the
same shell setting to the native job’s “Show toolchain” step and the e2e job’s
corresponding preparation step so failures propagate correctly on Windows.


# The generators are expected to be deterministic. A dirty tree here
# means generated output was committed stale, or differs by platform.
- name: Generated output is deterministic and committed
shell: bash
run: |
if ! git diff --exit-code; then
echo "::error::Generated output differs from what is committed. Run the prepare scripts and commit the result."
exit 1
fi
Comment on lines +59 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

git diff --exit-code does not detect new untracked files.

The step guarantees that generated output is committed. git diff --exit-code only compares tracked files. If a generator emits a new file that was never committed, the tree stays clean by this check and the defect passes. Use a porcelain status check instead.

🔧 Proposed fix
       - name: Generated output is deterministic and committed
         shell: bash
         run: |
-          if ! git diff --exit-code; then
+          git add --intent-to-add --all
+          if ! git diff --exit-code; then
             echo "::error::Generated output differs from what is committed. Run the prepare scripts and commit the result."
             exit 1
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Generated output is deterministic and committed
shell: bash
run: |
if ! git diff --exit-code; then
echo "::error::Generated output differs from what is committed. Run the prepare scripts and commit the result."
exit 1
fi
- name: Generated output is deterministic and committed
shell: bash
run: |
git add --intent-to-add --all
if ! git diff --exit-code; then
echo "::error::Generated output differs from what is committed. Run the prepare scripts and commit the result."
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 59 - 65, Update the “Generated output
is deterministic and committed” workflow step to use a porcelain Git status
check that detects both modified tracked files and newly generated untracked
files. Preserve the existing error message and nonzero exit behavior when the
working tree is not clean.


- name: Typecheck
run: pnpm typecheck

# Includes validate:source-bindings, which catches a pre-registration
# plan whose bound documents changed after it was reviewed. Fourteen
# such bindings had drifted undetected before this check existed.
- name: Test
run: pnpm test

- name: Build
run: pnpm build

- name: Validate game manifests
run: pnpm validate:manifests

native:
name: native / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

# rust-toolchain.toml pins the channel and the clippy/rustfmt components.
- name: Show toolchain
run: |
rustup show active-toolchain
cargo --version
cargo clippy --version

- uses: Swatinem/rust-cache@v2

- name: Format
run: cargo fmt --all -- --check

# Runs on both platforms deliberately. `clippy::large_enum_variant` on
# InstalledRuntime fires only on Windows, where PathBuf is 32 bytes
# against 24 on Linux, which pushes the variant gap past the 200-byte
# threshold. A Linux-only lint job would not have seen it.
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Test
run: cargo test --workspace

# Guards the `default-run` manifest key. Without it, adding a second
# binary to the crate makes every documented `cargo run -p vcg-host`
# invocation ambiguous, which silently broke the Raspberry Pi day-one
# bring-up script and eleven documented commands.
- name: Host doctor responds
run: cargo run -q -p vcg-host -- doctor

e2e:
name: e2e / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Prepare pinned assets and generated sources
run: |
pnpm prepare:assets
pnpm prepare:catalog
pnpm prepare:schemas

# playwright.config.ts pins `channel: "chrome"`, so the suite needs real
# Google Chrome rather than the bundled Chromium.
- name: Install Chrome for Playwright
run: pnpm --filter @vcg/console-lab exec playwright install --with-deps chrome

# The TV conformance suite measures real layout geometry. A 720p launcher
# text collision reached main because it only reproduced under Linux font
# metrics, which wrap a subtitle the Windows metrics fit on one line.
- name: End-to-end
run: pnpm test:e2e

- name: Upload failure traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces-${{ matrix.os }}
path: test-results/
retention-days: 7
if-no-files-found: ignore

pi-bringup:
name: pi bring-up script
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Enable Corepack
run: corepack enable

- uses: Swatinem/rust-cache@v2

# Runs the operator-facing script itself rather than reimplementing its
# steps, so the script cannot rot while its individual commands pass. It
# installs dependencies, prepares assets, builds, builds and runs the
# native host, then proves the real preview server serves the browser
# boundary the camera path depends on.
#
# The script warns that a non-aarch64 host is not Raspberry Pi evidence.
# That warning is correct: this job proves the script runs, not that the
# Pi is qualified.
- name: scripts/pi/bootstrap.sh
run: scripts/pi/bootstrap.sh
Comment on lines +191 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Invoke the script through bash so the run does not depend on the stored executable bit.

Direct invocation fails with "Permission denied" if scripts/pi/bootstrap.sh is not committed with mode 100755. Windows-based commits frequently drop that bit.

🔧 Proposed fix
       - name: scripts/pi/bootstrap.sh
-        run: scripts/pi/bootstrap.sh
+        run: bash scripts/pi/bootstrap.sh
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: scripts/pi/bootstrap.sh
run: scripts/pi/bootstrap.sh
- name: scripts/pi/bootstrap.sh
run: bash scripts/pi/bootstrap.sh
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 191 - 192, Update the CI step named
“scripts/pi/bootstrap.sh” to invoke the script explicitly through bash rather
than executing its path directly, while preserving the existing script path and
workflow behavior.

27 changes: 24 additions & 3 deletions apps/console-lab/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,12 @@ a:focus-visible {

.home-view {
display: grid;
grid-template-rows: minmax(230px, 1fr) auto auto;
/* The heading row's minimum is its own content, not a fixed height: a fixed
minimum lets the row be sized smaller than the text it holds, which spills
the trailing line into the destinations below it. The destinations row is
the flexible one -- it carries slack above its own min-height floor, so it
absorbs the shortfall on short viewports instead of being overlapped. */
grid-template-rows: minmax(min-content, 1fr) auto auto;
}

.home-heading h1,
Expand Down Expand Up @@ -4892,7 +4897,23 @@ kbd {
}

.home-view {
grid-template-rows: minmax(180px, 1fr) auto auto;
grid-template-rows: minmax(min-content, 1fr) auto auto;
}

/* Vertical rhythm scales with viewport height. At 720p the fixed desktop
spacing consumed more than the home view had to give, so the heading's
trailing line was pushed into the destinations. These floors keep the
rhythm intact on tall displays and reclaim it on short ones. */
.home-view .view-kicker {
margin-bottom: clamp(6px, 1.1vh, 18px);
}

.home-view .home-heading > p:last-child {
margin-top: clamp(8px, 1.5vh, 24px);
}

.home-view .destination-action {
margin-top: clamp(8px, 1.5vh, 18px);
}

.destination {
Expand Down Expand Up @@ -4934,7 +4955,7 @@ kbd {

.home-status {
gap: clamp(18px, 2vw, 76px);
padding-top: clamp(12px, 2vh, 43px);
padding-top: clamp(8px, 1.1vh, 43px);
}

.launch-screen {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"role": "investigation-acceptance-boundary",
"path": "docs/INVESTIGATIONS.md",
"sha256": "8f52b2b6400a15ae57ece003ab3e9fb552d2ccd7ee9b06b65bf09a4f5c8ee45e"
"sha256": "53a38288fb54951e17ae42c8ab4f7fe65c8fa2c5f416963932ce8ead99986e46"
},
{
"role": "camera-geometry-campaign-contract",
Expand All @@ -37,7 +37,7 @@
{
"role": "camera-geometry-baseline-plan",
"path": "benchmarks/camera-geometry/cross-tier-camera-placement-geometry-plan-v1.json",
"sha256": "97e863b605c3f79112a2dc826ac41716f0aae5df8ed27ee9518ba9e57abc03f6"
"sha256": "f58a4cc93a4e26a6e09500ebc881acba37327fef1d2051909eefd2b06d3990ca"
},
{
"role": "shared-camera-qualification-boundary",
Expand All @@ -47,7 +47,7 @@
{
"role": "physical-camera-state-boundary",
"path": "benchmarks/camera-state/physical-shutter-camera-state-experience-plan-v1.json",
"sha256": "997c7538bbfc0b377f17a609f3c179e9c97120db1cb46e630e1400f33140589b"
"sha256": "f13c2f7e8178d129bde1af96d650e02f91210c99c5499b61b0a7a642ff387ef9"
},
{
"role": "active-play-safety-boundary",
Expand All @@ -67,12 +67,12 @@
{
"role": "camera-cable-service-boundary",
"path": "benchmarks/camera-cabling/cross-tier-camera-cable-plan-v1.json",
"sha256": "e3ba9d8c889b09395e753be47a77e1e7846fa8b1415555706206716f1f069610"
"sha256": "5eb272f37f18249d02c8fdc70d817806df0dcce5bd81be4c60a817a92796007f"
},
{
"role": "idle-power-boundary",
"path": "benchmarks/idle-energy/cross-tier-idle-energy-plan-v1.json",
"sha256": "31af937413b02b916adedf946f01ee15f10fe52c9e4615a4641753e1c80af797"
"sha256": "87ad11a16f9a568b526f86254f10ab9492f7d97cd0554fd46a43bea44f59f9f9"
}
],
"baselineContract": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
{
"role": "camera-geometry-and-mount-boundary",
"path": "benchmarks/camera-geometry/cross-tier-camera-placement-geometry-plan-v1.json",
"sha256": "e9171e624dfb835c2a2b9170f895a38bf86fdb8d476f9f903b62c7a543bff8fa"
"sha256": "f58a4cc93a4e26a6e09500ebc881acba37327fef1d2051909eefd2b06d3990ca"
},
{
"role": "pi-radio-coexistence-boundary",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
{
"role": "room-and-play-zone-plan",
"path": "benchmarks/room-survey/living-room-play-zone-plan-v1.json",
"sha256": "e549ebd8aff6d266bdff57d5daac5ba3b66fd561fde409298c14bacfcf5e4558"
"sha256": "e801eb48091033482bf2fd3a0fb13814a2d489a2a181eaca58f6fae666d99b04"
},
{
"role": "shared-camera-plan",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"role": "investigation-acceptance-boundary",
"path": "docs/INVESTIGATIONS.md",
"sha256": "8f52b2b6400a15ae57ece003ab3e9fb552d2ccd7ee9b06b65bf09a4f5c8ee45e"
"sha256": "53a38288fb54951e17ae42c8ab4f7fe65c8fa2c5f416963932ce8ead99986e46"
},
{
"role": "substitute-qualification-question",
Expand All @@ -42,17 +42,17 @@
{
"role": "camera-cable-service-boundary",
"path": "benchmarks/camera-cabling/cross-tier-camera-cable-plan-v1.json",
"sha256": "e3ba9d8c889b09395e753be47a77e1e7846fa8b1415555706206716f1f069610"
"sha256": "5eb272f37f18249d02c8fdc70d817806df0dcce5bd81be4c60a817a92796007f"
},
{
"role": "camera-geometry-invalidation-boundary",
"path": "benchmarks/camera-geometry/cross-tier-camera-placement-geometry-plan-v1.json",
"sha256": "97e863b605c3f79112a2dc826ac41716f0aae5df8ed27ee9518ba9e57abc03f6"
"sha256": "f58a4cc93a4e26a6e09500ebc881acba37327fef1d2051909eefd2b06d3990ca"
},
{
"role": "physical-camera-state-boundary",
"path": "benchmarks/camera-state/physical-shutter-camera-state-experience-plan-v1.json",
"sha256": "997c7538bbfc0b377f17a609f3c179e9c97120db1cb46e630e1400f33140589b"
"sha256": "f13c2f7e8178d129bde1af96d650e02f91210c99c5499b61b0a7a642ff387ef9"
},
{
"role": "coordinate-frame-invalidation-boundary",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
{
"role": "motion-lab-camera-state-style",
"path": "apps/console-lab/src/styles.css",
"sha256": "e2ac55b94c96c2b0aaedfc62f315780994300661d14db9bf0822649df66af1ea"
"sha256": "b05667a1ca14e0384575ef85244c65667d0fa1feb5bba824c5face356ca8dd75"
},
{
"role": "camera-state-browser-proof",
Expand All @@ -58,17 +58,17 @@
{
"role": "camera-geometry-reach-visibility-boundary",
"path": "benchmarks/camera-geometry/cross-tier-camera-placement-geometry-plan-v1.json",
"sha256": "e9171e624dfb835c2a2b9170f895a38bf86fdb8d476f9f903b62c7a543bff8fa"
"sha256": "f58a4cc93a4e26a6e09500ebc881acba37327fef1d2051909eefd2b06d3990ca"
},
{
"role": "camera-cable-service-boundary",
"path": "benchmarks/camera-cabling/cross-tier-camera-cable-plan-v1.json",
"sha256": "ffadd375fb690ede91ce38b5c61cbfac0e1afc859890aaeef15eab233a758728"
"sha256": "5eb272f37f18249d02c8fdc70d817806df0dcce5bd81be4c60a817a92796007f"
},
{
"role": "idle-suspend-power-boundary",
"path": "benchmarks/idle-energy/cross-tier-idle-energy-plan-v1.json",
"sha256": "640597aabd70c6627f3f7dd862e527912c5c42f78483d24c00553227f6b9d085"
"sha256": "87ad11a16f9a568b526f86254f10ab9492f7d97cd0554fd46a43bea44f59f9f9"
},
{
"role": "active-play-safety-boundary",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
{
"path": "apps/console-lab/src/tracker.ts",
"sha256": "921ad16b2b133d6d9bb1a28ee7e58a56cadb31ee56cc0a10a5e24f06228d4a68"
"sha256": "46e0a312ce554398016d3e5ea7946d8d1f2c4ee49386493957b36ad1fe5d9919"
},
{
"path": "apps/console-lab/src/tracker-worker.ts",
Expand Down
Loading
Loading