Skip to content

Commit e459dca

Browse files
committed
feat(studio-bridge): add Docker image for Linux E2E testing
Pre-build a Docker image with Wine, Roblox Studio, and studio-bridge baked in. Eliminates fragile 5-10 min CI setup that re-installs everything each run. - Dockerfile uses studio-bridge's own `linux setup` command (no logic duplication) and pnpm deploy for self-contained runtime - Nightly build workflow pushes to ghcr.io with version tags - E2E workflow runs inside the container, skipping setup steps - devcontainer gains docker-in-docker for local image building
1 parent 327a3f1 commit e459dca

7 files changed

Lines changed: 228 additions & 11 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"version": "22",
1111
"pnpmVersion": "10.27.0"
1212
},
13-
"ghcr.io/devcontainers/features/github-cli:1": {}
13+
"ghcr.io/devcontainers/features/github-cli:1": {},
14+
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
1415
},
1516

1617
"customizations": {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: studio-linux-docker-build
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *' # Nightly 03:00 UTC
6+
workflow_dispatch:
7+
inputs:
8+
studio_version:
9+
description: 'Override Studio version hash (leave empty for latest)'
10+
required: false
11+
push:
12+
paths:
13+
- 'tools/studio-bridge/docker/**'
14+
- '.github/workflows/studio-linux-docker-build.yml'
15+
16+
env:
17+
REGISTRY: ghcr.io
18+
IMAGE_NAME: quenty/nevermore-studio-linux
19+
20+
jobs:
21+
resolve-version:
22+
runs-on: ubuntu-latest
23+
outputs:
24+
version: ${{ steps.resolve.outputs.version }}
25+
short: ${{ steps.resolve.outputs.short }}
26+
steps:
27+
- name: Resolve Studio version
28+
id: resolve
29+
run: |
30+
if [ -n "${{ inputs.studio_version }}" ]; then
31+
VERSION="${{ inputs.studio_version }}"
32+
else
33+
VERSION=$(curl -s https://clientsettingscdn.roblox.com/v2/client-version/WindowsStudio64 | jq -r .clientVersionUpload)
34+
fi
35+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
36+
echo "short=${VERSION:0:12}" >> "$GITHUB_OUTPUT"
37+
echo "Resolved Studio version: $VERSION"
38+
39+
check-existing:
40+
needs: resolve-version
41+
runs-on: ubuntu-latest
42+
outputs:
43+
exists: ${{ steps.check.outputs.exists }}
44+
steps:
45+
- name: Check if image already exists
46+
id: check
47+
run: |
48+
if docker manifest inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }} > /dev/null 2>&1; then
49+
echo "exists=true" >> "$GITHUB_OUTPUT"
50+
echo "Image already exists for version ${{ needs.resolve-version.outputs.version }}, skipping build"
51+
else
52+
echo "exists=false" >> "$GITHUB_OUTPUT"
53+
echo "Image not found, will build"
54+
fi
55+
env:
56+
DOCKER_CLI_EXPERIMENTAL: enabled
57+
58+
build-and-push:
59+
needs: [resolve-version, check-existing]
60+
if: needs.check-existing.outputs.exists != 'true'
61+
runs-on: ubuntu-latest
62+
permissions:
63+
contents: read
64+
packages: write
65+
steps:
66+
- name: Checkout repository
67+
uses: actions/checkout@v6
68+
69+
- name: Set up Docker Buildx
70+
uses: docker/setup-buildx-action@v3
71+
72+
- name: Log in to GitHub Container Registry
73+
uses: docker/login-action@v3
74+
with:
75+
registry: ${{ env.REGISTRY }}
76+
username: ${{ github.actor }}
77+
password: ${{ secrets.GITHUB_TOKEN }}
78+
79+
- name: Build and push image
80+
uses: docker/build-push-action@v6
81+
with:
82+
context: tools/studio-bridge/docker
83+
build-contexts: workspace=.
84+
build-args: |
85+
STUDIO_VERSION=${{ needs.resolve-version.outputs.version }}
86+
push: true
87+
tags: |
88+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
89+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }}
90+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.short }}
91+
cache-from: type=gha
92+
cache-to: type=gha,mode=max
93+
94+
cleanup:
95+
needs: build-and-push
96+
if: always() && needs.build-and-push.result == 'success'
97+
runs-on: ubuntu-latest
98+
permissions:
99+
packages: write
100+
steps:
101+
- name: Clean up old images
102+
uses: snok/container-retention-policy@v3.0.0
103+
with:
104+
account: quenty
105+
token: ${{ secrets.GITHUB_TOKEN }}
106+
image-names: nevermore-studio-linux
107+
cut-off: 30 days ago UTC
108+
keep-n-most-recent: 5

.github/workflows/studio-linux-e2e.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,27 @@ on:
88
- 'tools/studio-bridge/src/process/**'
99
- 'tools/studio-bridge/src/commands/linux/**'
1010
- 'tools/nevermore-cli-helpers/src/auth/**'
11+
- 'tools/studio-bridge/docker/**'
1112
- '.github/workflows/studio-linux-e2e.yml'
1213

1314
jobs:
1415
studio-linux-e2e:
1516
runs-on: ubuntu-latest
1617
timeout-minutes: 30
18+
container:
19+
image: ghcr.io/quenty/nevermore-studio-linux:latest
20+
credentials:
21+
username: ${{ github.actor }}
22+
password: ${{ secrets.GITHUB_TOKEN }}
23+
options: --user studio
24+
env:
25+
ROBLOSECURITY: ${{ secrets.ROBLOSECURITY }}
1726
steps:
1827
- name: Checkout repository
1928
uses: actions/checkout@v6
2029

21-
- name: Setup node
22-
uses: actions/setup-node@v6
23-
with:
24-
node-version: '21'
25-
2630
- name: Setup pnpm
2731
uses: pnpm/action-setup@v4
28-
with:
29-
cache: true
3032

3133
- name: Setup registries
3234
run: |
@@ -55,9 +57,6 @@ jobs:
5557
run: npm install --ignore-scripts -g .
5658
working-directory: tools/studio-bridge
5759

58-
- name: Setup Linux environment
59-
run: studio-bridge linux setup --install-deps
60-
6160
- name: Verify environment health (pre-auth)
6261
run: studio-bridge linux status
6362

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Primary build context is this directory — only entrypoint.sh is needed.
2+
# The workspace named build context handles repo files.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# syntax=docker/dockerfile:1
2+
FROM ubuntu:24.04
3+
ARG STUDIO_VERSION
4+
ARG DEBIAN_FRONTEND=noninteractive
5+
6+
# --- System deps (cached layer, rarely changes) ---
7+
# Mirrors linux-prerequisites.ts:installDependenciesAsync()
8+
RUN dpkg --add-architecture i386 \
9+
&& apt-get update \
10+
&& apt-get install -y --no-install-recommends \
11+
ca-certificates curl gnupg software-properties-common \
12+
xvfb openbox mesa-utils \
13+
gcc-mingw-w64-x86-64 unzip procps \
14+
# WineHQ repo for Wine 11+ (same logic as linux-prerequisites.ts:91-128)
15+
&& mkdir -pm755 /etc/apt/keyrings \
16+
&& curl -sL https://dl.winehq.org/wine-builds/winehq.key \
17+
-o /etc/apt/keyrings/winehq-archive.key \
18+
&& curl -sfL https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources \
19+
-o /etc/apt/sources.list.d/winehq-noble.sources \
20+
&& apt-get update \
21+
&& apt-get install -y --no-install-recommends winehq-stable \
22+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
23+
24+
# --- Node.js 22 LTS ---
25+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
26+
&& apt-get install -y --no-install-recommends nodejs \
27+
&& corepack enable pnpm \
28+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
29+
30+
# --- Non-root user ---
31+
RUN useradd -m -s /bin/bash studio
32+
USER studio
33+
WORKDIR /home/studio
34+
35+
# --- Build studio-bridge from source (via named build context "workspace") ---
36+
COPY --from=workspace --chown=studio:studio package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.json /home/studio/build/
37+
COPY --from=workspace --chown=studio:studio tools/ /home/studio/build/tools/
38+
WORKDIR /home/studio/build
39+
RUN pnpm install --frozen-lockfile --filter "@quenty/studio-bridge..." \
40+
&& pnpm -r --filter "@quenty/studio-bridge..." run build
41+
42+
# --- Run studio-bridge to set up Studio (single source of truth!) ---
43+
# Invoke cli.js directly — workspace deps are resolved by pnpm in node_modules.
44+
RUN node tools/studio-bridge/dist/src/cli/cli.js linux setup \
45+
${STUDIO_VERSION:+--studio-version "$STUDIO_VERSION"}
46+
47+
# --- Install studio-bridge globally for runtime, then clean up ---
48+
# Use pnpm deploy to create a self-contained copy with resolved workspace deps,
49+
# then link the binary. This avoids npm registry lookups for workspace packages.
50+
RUN pnpm --filter "@quenty/studio-bridge" deploy --legacy --prod /home/studio/.studio-bridge \
51+
&& mkdir -p /home/studio/.npm-global/bin \
52+
&& ln -s /home/studio/.studio-bridge/dist/src/cli/cli.js /home/studio/.npm-global/bin/studio-bridge \
53+
&& chmod +x /home/studio/.studio-bridge/dist/src/cli/cli.js \
54+
&& rm -rf /home/studio/build
55+
56+
# --- Environment (matches linux-wine-env.ts:buildWineEnv) ---
57+
ENV STUDIO_DIR=/home/studio/roblox-studio \
58+
WINEPREFIX=/home/studio/.wine \
59+
DISPLAY=:99 \
60+
WINEDEBUG=-all \
61+
WINEARCH=win64 \
62+
WINEDLLOVERRIDES="mscoree=d;mshtml=d" \
63+
MESA_GL_VERSION_OVERRIDE=4.5 \
64+
MESA_GLSL_VERSION_OVERRIDE=450 \
65+
NPM_CONFIG_PREFIX=/home/studio/.npm-global \
66+
PATH=/home/studio/.npm-global/bin:$PATH
67+
68+
COPY --chown=studio:studio entrypoint.sh /home/studio/entrypoint.sh
69+
RUN chmod +x /home/studio/entrypoint.sh
70+
WORKDIR /home/studio
71+
ENTRYPOINT ["/home/studio/entrypoint.sh"]
72+
CMD ["bash"]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
studio:
3+
build:
4+
context: .
5+
additional_contexts:
6+
workspace: ../../..
7+
args:
8+
STUDIO_VERSION: ${STUDIO_VERSION:-}
9+
image: ghcr.io/quenty/nevermore-studio-linux:${STUDIO_VERSION:-latest}
10+
environment:
11+
- ROBLOSECURITY=${ROBLOSECURITY:-}
12+
volumes:
13+
- ../../../:/workspace
14+
- wine-prefix:/home/studio/.wine
15+
working_dir: /workspace
16+
stdin_open: true
17+
tty: true
18+
19+
volumes:
20+
wine-prefix:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
# Start Xvfb + openbox (mirrors linux-display-manager.ts), then exec user command.
3+
set -euo pipefail
4+
5+
if ! pgrep -x Xvfb > /dev/null 2>&1; then
6+
Xvfb "${DISPLAY:-:99}" -screen 0 1024x768x24 &
7+
sleep 0.5
8+
fi
9+
10+
if ! pgrep -x openbox > /dev/null 2>&1; then
11+
DISPLAY="${DISPLAY:-:99}" openbox &
12+
sleep 0.5
13+
fi
14+
15+
exec "$@"

0 commit comments

Comments
 (0)