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
6 changes: 6 additions & 0 deletions .github/actions/restore-cargo-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ description: Restore the build tree the check workflow's Rust lane saves.
#
# `registry/src` is deliberately absent: it is unpacked from `registry/cache` on
# demand, so caching both stores every dependency twice.
# `target/desktop` is the desktop app's build, which only the Rust lane reads
# and which it caches in an entry of its own.
#
# Keyed on the lockfile, so a dependency bump rebuilds and a source edit does
# not, and on what compiles every object in target/: rustc, the C++ compiler
Expand All @@ -25,6 +27,9 @@ outputs:
key:
description: The exact key, for the step that saves the tree.
value: ${{ steps.key.outputs.key }}
prefix:
description: The key without the lockfile, for a cache built the same way.
value: ${{ steps.key.outputs.prefix }}
cache-hit:
description: Whether the exact key was restored.
value: ${{ steps.restore.outputs.cache-hit }}
Expand Down Expand Up @@ -59,5 +64,6 @@ runs:
~/.cargo/registry/cache
~/.cargo/git/db
target
!target/desktop
key: ${{ steps.key.outputs.key }}
restore-keys: ${{ steps.key.outputs.prefix }}
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ updates:
directory: /
schedule:
interval: weekly
# The desktop app's own lockfile. Tauri and its plugins are pinned exactly in
# desktop/Cargo.toml, so they move only when that file is edited on
# purpose; this keeps everything beneath them current, in one group so the
# GTK and WebView crates that have to agree move together.
- package-ecosystem: cargo
directory: /desktop
schedule:
interval: weekly
groups:
desktop:
patterns:
- "*"
- package-ecosystem: npm
directory: /
schedule:
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,41 @@ jobs:
id: cargo-cache
uses: ./.github/actions/restore-cargo-cache

# The desktop crate's cache key: the cargo cache's compilers and flags,
# under a name of its own and with its own lockfile.
- name: Compute the desktop cache key
if: matrix.lane == 'rust'
id: desktop
env:
CARGO_PREFIX: ${{ steps.cargo-cache.outputs.prefix }}
INPUTS: ${{ hashFiles('desktop/Cargo.lock') }}
run: |
prefix="desktop-${CARGO_PREFIX#cargo-}"
echo "prefix=$prefix" >>"$GITHUB_OUTPUT"
echo "key=$prefix$INPUTS" >>"$GITHUB_OUTPUT"

# What desktop/ links against. Installed on every run of this lane, not
# only when a pull request touches desktop/: the `desktop` gate skips
# without them, and under CI a gate that skips fails the run, so an
# unrelated pull request would fail here.
- name: Install the desktop app's build dependencies
if: matrix.lane == 'rust'
uses: ./.github/actions/apt-install
with:
packages: libwebkit2gtk-4.1-dev libxdo-dev libssl-dev librsvg2-dev

# The desktop crate's build, in an entry of its own. The cargo cache is
# also what every mutation shard restores, and a path added to it would
# be a gigabyte of WebKit bindings each shard downloads and never reads.
- name: Restore desktop cargo cache
id: desktop-cache
if: matrix.lane == 'rust'
uses: actions/cache/restore@v6
with:
path: target/desktop
key: ${{ steps.desktop.outputs.key }}
restore-keys: ${{ steps.desktop.outputs.prefix }}

# Every lane fetches and verifies these bytes; only the portable lane
# saves them, below, so three lanes do not race to write one key.
- name: Restore vendored bytes
Expand Down Expand Up @@ -380,8 +415,19 @@ jobs:
~/.cargo/registry/cache
~/.cargo/git/db
target
!target/desktop
key: ${{ steps.cargo-cache.outputs.key }}

- name: Save desktop cargo cache
if: >-
${{ !cancelled() && github.event_name != 'merge_group'
&& matrix.lane == 'rust'
&& steps.desktop-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v6
with:
path: target/desktop
key: ${{ steps.desktop.outputs.key }}

# The one status: every gate lane and audit passed, and on a PR the mutation
# plan and every shard did too. Planning only runs on pull requests, and the
# shards only after a plan that found mutants, so a skipped shard job after a
Expand Down
215 changes: 215 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# The desktop app's installers, built on the three platforms they are for.
#
# A workflow of its own rather than a leg of check.yml's `build`, because the
# two publish different things on different schedules: `build` refreshes the
# rolling CLI release on every push to main, and a desktop release is a
# version somebody chose to cut. Nothing here touches the CLI artifacts or the
# `latest` release.
#
# Pull requests run it only when the desktop app itself changed, because each
# leg builds the server in release mode as well as the app around it. A change
# to the server's side of the launch contract is still caught on Linux: the
# check job's Rust lane compiles and tests desktop/ on every run.
name: desktop

on:
pull_request:
paths:
- desktop/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This workflow skips desktop packaging when server or embedded-page code changes. Add the root server and web inputs to both path filters; otherwise a PR can change the binary bundled into the desktop app without any cross-platform installer or smoke check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/desktop.yml, line 18:

<comment>This workflow skips desktop packaging when server or embedded-page code changes. Add the root server and web inputs to both path filters; otherwise a PR can change the binary bundled into the desktop app without any cross-platform installer or smoke check.</comment>

<file context>
@@ -0,0 +1,218 @@
+on:
+  pull_request:
+    paths:
+      - desktop/**
+      - scripts/desktop-smoke.sh
+      - .github/workflows/desktop.yml
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partly fixed in 658f1ea. The check job now builds and tests desktop/ on Linux whenever src/main.rs changes, which is where the launch contract lives (the flags the app passes and the line it waits for, pinned by a desktop test). The three-platform packaging stays on desktop paths: each leg is a release WebRTC build, and running it for every server change would cost three of them per pull request.

- scripts/desktop-smoke.sh
- .github/workflows/desktop.yml
push:
branches: [main]
paths:
- desktop/**
- scripts/desktop-smoke.sh
- .github/workflows/desktop.yml
tags: ['desktop-v*']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: desktop-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main' }}

env:
# The CLI that bundles, pinned like every other tool the workflows run. Kept
# on the same minor as the `tauri` crate in desktop/Cargo.toml.
TAURI_CLI_VERSION: 2.12.3

jobs:
package:
name: ${{ matrix.name }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# 22.04 rather than the 24.04 the gates use: the bundle links the
# WebKitGTK it is built against, and the oldest one an installer
# should still start on is the floor. 22.04 is the first Ubuntu with
# WebKitGTK 4.1, which Tauri 2 requires.
- name: Linux x86_64
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
extension: ''
bundles: deb,rpm,appimage
pr_bundles: deb
- name: macOS arm64
os: macos-15
target: aarch64-apple-darwin
extension: ''
bundles: app,dmg
pr_bundles: dmg
- name: Windows x86_64
os: windows-2025
target: x86_64-pc-windows-msvc
extension: .exe
bundles: msi,nsis
pr_bundles: nsis
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7

- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: >-
sudo apt-get update && sudo apt-get install -y libglib2.0-dev
libwebkit2gtk-4.1-dev libxdo-dev libssl-dev librsvg2-dev file xvfb
- name: Install WebRTC C++ compiler
if: runner.os == 'Linux'
uses: ./.github/actions/webrtc-clang

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: desktop-${{ matrix.target }}
workspaces: |
. -> target
desktop -> target/desktop

# The server embeds the page and its vendored runtimes, so the same
# fetch and check the CLI release makes, for the same reason: a missing
# asset compiles into a server that boots and then strands the candidate.
- name: Restore vendored bytes
uses: ./.github/actions/restore-vendor
- name: Fetch browser assets
run: ./scripts/fetch-vendor.sh
shell: bash
- name: Verify the assets about to be embedded
run: ./scripts/verify-vendor.sh
shell: bash

# The flags check.yml's release build passes, for the reasons it gives:
# a static CRT on Windows, and `-ObjC` so WebRTC's Objective-C categories
# survive the link on macOS. Here and not job-wide, because they are the
# server's and the app around it links without them.
- name: Build the server
shell: bash
env:
RUSTFLAGS: >-
${{ matrix.target == 'x86_64-pc-windows-msvc' && '-C target-feature=+crt-static'
|| contains(matrix.target, '-apple-') && '-Clink-arg=-ObjC'
|| '' }}
run: cargo build --release --locked --target ${{ matrix.target }}

# Where tauri.bundle.json's `externalBin` looks: the name Tauri expects
# carries the target triple, and the installer drops it again.
- name: Place the server beside the app
shell: bash
run: |
mkdir -p target/desktop/sidecar
Comment thread
jserv marked this conversation as resolved.
cp target/${{ matrix.target }}/release/codetrial${{ matrix.extension }} \
target/desktop/sidecar/codetrial-${{ matrix.target }}${{ matrix.extension }}

# A pull request builds one installer per platform, which is enough to
# prove the bundle config still packages; each further format downloads
# its own packaging tool on every run.
#
# `NO_STRIP`, because the AppImage tool's own strip is older than the
# toolchain and fails on sections it does not know. Both programs are
# already stripped by their release profiles, so nothing is lost.
- name: Build the installers
working-directory: desktop
shell: bash
env:
NO_STRIP: 'true'
run: >-
npx --yes "@tauri-apps/cli@$TAURI_CLI_VERSION" build --ci
--target ${{ matrix.target }}
--bundles ${{ github.event_name == 'pull_request' && matrix.pr_bundles || matrix.bundles }}
--config tauri.bundle.json

# The built app, not the installer, but with the server beside it the
# way every installer puts it.
- name: Start the app, then kill it
shell: bash
run: |
app=target/desktop/${{ matrix.target }}/release/codetrial-desktop${{ matrix.extension }}
if [ "$RUNNER_OS" = Linux ]; then
xvfb-run -a sh scripts/desktop-smoke.sh "$app"
else
sh scripts/desktop-smoke.sh "$app"
fi

- uses: actions/upload-artifact@v7
with:
name: codetrial-desktop-${{ matrix.target }}
if-no-files-found: error
path: |
target/desktop/${{ matrix.target }}/release/bundle/deb/*.deb
target/desktop/${{ matrix.target }}/release/bundle/rpm/*.rpm
target/desktop/${{ matrix.target }}/release/bundle/appimage/*.AppImage
target/desktop/${{ matrix.target }}/release/bundle/dmg/*.dmg
target/desktop/${{ matrix.target }}/release/bundle/msi/*.msi
target/desktop/${{ matrix.target }}/release/bundle/nsis/*.exe

# A tag names a version, and the version is the one desktop/Cargo.toml
# says: an installer reporting 0.1.0 under a desktop-v0.2.0 release is the
# mismatch this refuses before anything is published.
release:
needs: package
if: startsWith(github.ref, 'refs/tags/desktop-v')
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
pattern: codetrial-desktop-*
path: dist
merge-multiple: true
- name: Publish the release
shell: bash
run: |
tag=${GITHUB_REF#refs/tags/}
version=$(sed -n 's/^version *= *"\([^"]*\)"$/\1/p' desktop/Cargo.toml | head -n 1)
[ "$tag" = "desktop-v$version" ] || {
echo "tag $tag does not match desktop/Cargo.toml version $version" >&2
exit 1
}
ls dist
cat > notes.md <<EOF
Desktop build of ${GITHUB_SHA:0:7}. The installers are not signed yet.

- macOS: the first launch is refused as from an unidentified developer.
Allow it under System Settings, Privacy & Security, Open Anyway.
- Windows: SmartScreen warns before the first launch.
- Linux: needs WebKitGTK 4.1. Where that WebKitGTK is built without
WebRTC, the interview opens in the default browser and the app keeps
its server running.

docs/desktop.md describes what the app does and what it has not been
qualified for yet.
EOF
gh release create "$tag" --prerelease --verify-tag \
--title "CodeTrial desktop $version" --notes-file notes.md dist/*
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ test = true

# scripts/hook-check holds every `cargo build` to the git hooks being in
# effect; its build.rs says why it is a member of its own. `vendor/` holds path
# dependencies, not members.
# dependencies, not members, and `desktop/` is a crate of its own with its own
# lockfile that the root's builds must not need WebKitGTK to compile.
[workspace]
members = ["scripts/hook-check"]
default-members = [".", "scripts/hook-check"]
exclude = ["vendor"]
exclude = ["vendor", "desktop"]

# Release archives distribute the executable, not a debugging target. Cargo
# already omits debug information in this profile; omit linker symbols too.
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ rather than guessing.
| Phase | Required | Notes |
|---|---|---|
| Build time | Rust 1.98 or newer (rustup selects the release `rust-toolchain.toml` pins), `curl` or `wget`, and `sha256sum` or `shasum`; Linux also needs Clang 21+ and GLib development headers | Cargo resolves application crates from `Cargo.lock`. `make build` fetches the checksum-pinned browser assets on the first build. |
| Test time | Build-time tools, Node.js 18+, and Python 3.9+ | Node runs browser and fixture checks; Python verifies generated problem-bank files. `npm ci` adds pinned Prettier, ESLint and Playwright for the full local browser gate. `ruff`, `shellcheck`, `shfmt`, `commentflow`, `actionlint`, `cargo-audit`, and `cargo-deny` are optional: each gate reports that it skipped rather than failing without them; locally the `actionlint` lane also takes its container image. CI installs these tools and enforces its test lanes whatever a checkout can run. The independent [dependency security check](docs/development.md#dependency-security-checks) blocks newly introduced vulnerabilities on PRs and runs full audits on main. |
| Test time | Build-time tools, Node.js 18+, and Python 3.9+ | Node runs browser and fixture checks; Python verifies generated problem-bank files. `npm ci` adds pinned Prettier, ESLint and Playwright for the full local browser gate. `ruff`, `shellcheck`, `shfmt`, `commentflow`, `actionlint`, `cargo-audit`, `cargo-deny`, and on Linux `libwebkit2gtk-4.1-dev` (the desktop app's clippy and tests) are optional: each gate reports that it skipped rather than failing without them; locally the `actionlint` lane also takes its container image. CI installs these tools and enforces its test lanes whatever a checkout can run. The independent [dependency security check](docs/development.md#dependency-security-checks) blocks newly introduced vulnerabilities on PRs and runs full audits on main. |
| Runtime | The compiled `codetrial` binary and a config file | No Node.js, Python, or `node_modules` is required. Rust dependencies are compiled into the binary; browser dependencies are vendored, checksum-pinned, and embedded, so a `web/` directory is optional and only overrides what is already inside. |

On Linux x86_64, `make` downloads the checksum-pinned WebRTC compiler into
Expand Down Expand Up @@ -90,16 +90,19 @@ file. Extra `codetrial.env.*` files in the same directory are optional providers
used for pooling.

If `web` mode finds no config file at all, it serves a Setup page and prints the
URL to open, so steps 2 and 3 above can be skipped: fill in your keys there and
it writes `config/codetrial.env.local` for you. That page serves on a loopback
URL to open, so steps 2 and 3 above can be skipped: fill in your keys there, or
import them from a config file saved earlier, and it writes
`config/codetrial.env.local` for you. That page serves on a loopback
address only and refuses any other, so writing the file yourself is the way to
deploy for other people. See [docs/install.md](docs/install.md).

## Run

Prebuilt binaries for Linux, macOS, and Windows are published for every build of
`main`. What they carry, the config file each needs beside it, and the platform
notes are in [docs/install.md](docs/install.md). From a checkout instead:
notes are in [docs/install.md](docs/install.md). A desktop app that starts the
server itself and shows it in its own window is described in
[docs/desktop.md](docs/desktop.md). From a checkout instead:

```bash
INTERVIEW_ROOM_NAME=interview-local make web
Expand Down Expand Up @@ -318,6 +321,8 @@ The common ones:
| `CODETRIAL_GEMINI_CANDIDATE_VIDEO_ENABLED` | `false` | Forward candidate video to Gemini, one low-resolution frame in five seconds; ignored in a whiteboard interview |
| `CODETRIAL_COMPILER_EXPLORER_ENABLED` | `true` | Enable remote C, C++, and Java runs |
| `CODETRIAL_MAX_CONCURRENT_INTERVIEWS` | `16` | Interviews one `web` process hosts agents for |
| `CODETRIAL_ALLOWED_HOSTS` | empty | Comma-separated `Host` values the server answers; any other is refused with 421. Empty answers every name, but a value that names no host is refused at startup. `--allowed-hosts` sets it from the command line |
| `CODETRIAL_EXIT_WITH_STDIN` | unset | For a program that starts the server as its child: exit when standard input reaches end of file, so the server does not outlive a launcher that crashed. Read from the process environment even under `--data-dir` |

Two more matter in production. `SESSION_SECRET` signs the session cookie, and
its built-in default is a string published in this repository, so the server
Expand Down Expand Up @@ -373,6 +378,7 @@ recorded rather than left implicit. See
| [Contributing](CONTRIBUTING.md) | Reporting a bug, the commit and title rules, opening a pull request |
| [Development](docs/development.md) | The gate, formatters, hooks, generated files, releases |
| [Installing a binary](docs/install.md) | Published binaries, platform notes, the config beside them |
| [Desktop app](docs/desktop.md) | The macOS, Windows and Linux app, its data folder, and what it has not been qualified for |
| [Interview length](docs/interview-length.md) | What the lobby offers and what the endpoints enforce |
| [Integrity evidence](docs/integrity-evidence.md) | Response windows, and what CodeTrial declines to look for |
| [Third-party notices](THIRD-PARTY-NOTICES.md) | Bundled software, licenses, checksums |
Expand All @@ -394,6 +400,7 @@ recorded rather than left implicit. See

```text
src/ Rust web server, interviewer, and problem bank
desktop/ Desktop app that runs the server in its own window
web/ Static browser application
problem-bank/ Problem and judge source of truth
config/ Env file templates and per-provider credentials
Expand Down
Loading
Loading