Skip to content

Commit 3e307f2

Browse files
committed
fix(studio-bridge): sync action modules before execute in process run
StudioBridgeServer.executeAsync() sent the execute message without first registering the execute.luau action handler with the plugin, causing UNKNOWN_REQUEST errors. Add _ensureActionsAsync() to sync dynamic action modules (via syncActions + registerAction) before the first execute call, matching the pattern already used by BridgeSession.
1 parent 088a5db commit 3e307f2

File tree

4 files changed

+97
-31
lines changed

4 files changed

+97
-31
lines changed

.github/workflows/studio-linux-docker-build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
description: 'Override Studio version hash (leave empty for latest)'
1010
required: false
1111
push:
12+
branches: [main]
1213
paths:
1314
- 'tools/studio-bridge/docker/**'
1415
- '.github/workflows/studio-linux-docker-build.yml'
@@ -57,7 +58,7 @@ jobs:
5758

5859
build-and-push:
5960
needs: [resolve-version, check-existing]
60-
if: needs.check-existing.outputs.exists != 'true'
61+
if: needs.check-existing.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
6162
runs-on: ubuntu-latest
6263
permissions:
6364
contents: read

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

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,9 @@ jobs:
4444
run: pnpm install --frozen-lockfile
4545

4646
- name: Setup Aftman
47-
run: |
48-
mkdir -p ~/.aftman/bin
49-
curl -fsSL https://github.com/LPGhatguy/aftman/releases/download/v0.3.0/aftman-0.3.0-linux-x86_64.zip -o /tmp/aftman.zip
50-
unzip -o /tmp/aftman.zip -d /tmp/aftman
51-
install -m 755 /tmp/aftman/aftman ~/.aftman/bin/aftman
52-
echo "$HOME/.aftman/bin" >> "$GITHUB_PATH"
53-
export PATH="$HOME/.aftman/bin:$PATH"
54-
aftman install --no-trust-check
55-
env:
56-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
uses: ok-nick/setup-aftman@v0.4.2
48+
with:
49+
token: ${{ secrets.GITHUB_TOKEN }}
5750

5851
- name: Build all tools
5952
run: pnpm -r --filter './tools/**' run build
@@ -78,33 +71,21 @@ jobs:
7871
env:
7972
ROBLOSECURITY: ${{ secrets.ROBLOSECURITY }}
8073

81-
- name: Collect Studio logs
74+
- name: Print logs
8275
if: always()
8376
run: |
84-
mkdir -p /tmp/studio-logs
85-
cp /tmp/studio-bridge-wine.log /tmp/studio-logs/wine.log 2>/dev/null || true
86-
# Roblox Studio internal logs (inside Wine prefix)
87-
find ~/.wine/drive_c/users/ -path "*/Roblox/logs/*" -name "*.log" -exec cp {} /tmp/studio-logs/ \; 2>/dev/null || true
88-
# Also check for crash dumps or error logs
89-
find ~/.wine/drive_c/users/ -path "*/Roblox/*.log" -exec cp {} /tmp/studio-logs/ \; 2>/dev/null || true
90-
# List what we found
91-
echo "=== Collected log files ==="
92-
ls -la /tmp/studio-logs/ 2>/dev/null || echo "No logs found"
93-
echo ""
9477
echo "=== Wine log (last 100 lines) ==="
95-
tail -100 /tmp/studio-logs/wine.log 2>/dev/null || echo "No Wine log"
78+
tail -100 /tmp/studio-bridge-wine.log 2>/dev/null || echo "No Wine log"
9679
echo ""
9780
echo "=== Studio logs ==="
98-
for f in /tmp/studio-logs/*.log; do
99-
[ "$f" = "/tmp/studio-logs/wine.log" ] && continue
100-
echo "--- $f ---"
101-
tail -50 "$f" 2>/dev/null
102-
done
81+
tail -50 ~/.wine/drive_c/users/*/AppData/Local/Roblox/logs/*.log 2>/dev/null || echo "No Studio logs"
10382
10483
- name: Upload logs
10584
if: always()
10685
uses: actions/upload-artifact@v4
10786
with:
10887
name: studio-bridge-logs
109-
path: /tmp/studio-logs/
88+
path: |
89+
/tmp/studio-bridge-wine.log
90+
~/.wine/drive_c/users/*/AppData/Local/Roblox/logs/
11091
if-no-files-found: ignore

tools/studio-bridge/docker/Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ RUN dpkg --add-architecture i386 \
2121
&& apt-get install -y --no-install-recommends winehq-stable \
2222
&& apt-get clean && rm -rf /var/lib/apt/lists/*
2323

24-
# --- Node.js 22 LTS ---
24+
# --- Node.js 22 LTS + GitHub CLI (needed by setup-aftman action) ---
2525
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
26-
&& apt-get install -y --no-install-recommends nodejs \
26+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
27+
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
28+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
29+
> /etc/apt/sources.list.d/github-cli.list \
30+
&& apt-get update \
31+
&& apt-get install -y --no-install-recommends nodejs gh \
2732
&& corepack enable pnpm \
2833
&& apt-get clean && rm -rf /var/lib/apt/lists/*
2934

tools/studio-bridge/src/server/studio-bridge-server.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import {
3030
decodePluginMessage,
3131
} from './web-socket-protocol.js';
3232
import { ActionDispatcher } from './action-dispatcher.js';
33+
import {
34+
loadActionSourcesAsync,
35+
type ActionSource,
36+
} from '../commands/framework/action-loader.js';
3337
import {
3438
injectPluginAsync,
3539
type InjectedPlugin,
@@ -218,6 +222,8 @@ export class StudioBridgeServer {
218222
private _negotiatedCapabilities: Capability[] = ['execute'];
219223
private _lastHeartbeatTimestamp: number | undefined;
220224
private _actionDispatcher = new ActionDispatcher();
225+
private _actionsReady = false;
226+
private _actionSources: ActionSource[] | undefined;
221227

222228
constructor(options: StudioBridgeServerOptions = {}) {
223229
this._options = options;
@@ -420,6 +426,10 @@ export class StudioBridgeServer {
420426
throw new Error('Cannot execute: no connected client');
421427
}
422428

429+
// Sync action modules before first execute (state must still be 'ready'
430+
// because performActionAsync checks for it).
431+
await this._ensureActionsAsync();
432+
423433
this._state = 'executing';
424434
this._onPhase?.('executing');
425435

@@ -483,6 +493,75 @@ export class StudioBridgeServer {
483493
this._state = 'stopped';
484494
}
485495

496+
// -----------------------------------------------------------------------
497+
// Private: _ensureActionsAsync
498+
// -----------------------------------------------------------------------
499+
500+
/**
501+
* Ensure action modules (like `execute.luau`) are synced to the plugin
502+
* before first use. Uses `syncActions` to check which actions the plugin
503+
* is missing, then registers them via `registerAction`.
504+
*
505+
* Only works with v2 plugins. v1 plugins skip this step (they would need
506+
* actions baked in, which the current plugin architecture does not support).
507+
*/
508+
private async _ensureActionsAsync(): Promise<void> {
509+
if (this._actionsReady) return;
510+
511+
if (this._negotiatedProtocolVersion < 2) {
512+
this._actionsReady = true;
513+
return;
514+
}
515+
516+
if (!this._actionSources) {
517+
this._actionSources = await loadActionSourcesAsync();
518+
OutputHelper.verbose(
519+
`[StudioBridge] Loaded ${this._actionSources.length} action source(s): ${this._actionSources.map((a) => a.name).join(', ') || '(none)'}`,
520+
);
521+
}
522+
523+
if (this._actionSources.length === 0) {
524+
this._actionsReady = true;
525+
return;
526+
}
527+
528+
const actions: Record<string, string> = {};
529+
for (const action of this._actionSources) {
530+
actions[action.name] = action.hash;
531+
}
532+
533+
OutputHelper.verbose('[StudioBridge] Syncing actions with plugin');
534+
const syncResult = await this.performActionAsync<PluginMessage>({
535+
type: 'syncActions',
536+
payload: { actions },
537+
}, 10_000);
538+
539+
if (syncResult.type === 'syncActionsResult') {
540+
const needed = (syncResult.payload as Record<string, unknown>).needed as string[];
541+
OutputHelper.verbose(
542+
`[StudioBridge] ${needed.length} action(s) need registering${needed.length > 0 ? ': ' + needed.join(', ') : ''}`,
543+
);
544+
545+
for (const actionName of needed) {
546+
const action = this._actionSources.find((a) => a.name === actionName);
547+
if (!action) continue;
548+
549+
OutputHelper.verbose(`[StudioBridge] Registering action: ${actionName}`);
550+
await this.performActionAsync<PluginMessage>({
551+
type: 'registerAction',
552+
payload: {
553+
name: action.name,
554+
source: action.source,
555+
hash: action.hash,
556+
},
557+
}, 10_000);
558+
}
559+
}
560+
561+
this._actionsReady = true;
562+
OutputHelper.verbose('[StudioBridge] Action sync complete');
563+
}
564+
486565
// -----------------------------------------------------------------------
487566
// Private: _injectPluginAsync
488567
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)