Skip to content

Commit 923e067

Browse files
rentziassCopilot
andauthored
Add 'Connect to Actions Job Debugger' command
Registers a new debug adapter type (github-actions-job) and a command-palette command that connects to an Actions runner's Dev Tunnel endpoint using DAP-over-WebSocket. The flow: 1. User runs 'GitHub Actions: Connect to Actions Job Debugger...' 2. Prompted for the wss:// tunnel URL (manual entry for now) 3. Extension acquires the current GitHub auth token 4. Opens a WebSocket to the tunnel with Authorization: Bearer <token> 5. Launches a standard VS Code debug session using an inline adapter The WebSocketDapAdapter implements vscode.DebugAdapter and forwards DAP JSON messages as individual text WebSocket frames — matching the runner's WebSocketDapBridge and the gh-actions-debugger CLI bridge. Keepalive pings run every 25s. On remote close, a DAP 'terminated' event is synthesized so VS Code tears down the session cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 53f7bbe commit 923e067

File tree

8 files changed

+451
-2
lines changed

8 files changed

+451
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# GitHub Actions for VS Code
22

33
> **🐛 Actions Job Debugger (Preview):** To try the latest debugger build, download the `.vsix` artifact from the most recent [Build Debugger Extension](https://github.com/github/vscode-github-actions/actions/workflows/debugger-build.yml) workflow run. On the workflow run page, scroll to **Artifacts** and download **vscode-github-actions-debugger**. Then install it in VS Code by running `code --install-extension <path-to-downloaded.vsix>` or via the Extensions view → `` menu → **Install from VSIX…**.
4+
>
5+
> Once installed, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run **GitHub Actions: Connect to Actions Job Debugger…**. Paste the `wss://` tunnel URL from a debug-mode job and the extension will open a full debug session using your current GitHub credentials.
46
57
The GitHub Actions extension lets you manage your workflows, view the workflow run history, and helps with authoring workflows.
68

package-lock.json

Lines changed: 49 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"activationEvents": [
2626
"onView:workflows",
2727
"onView:settings",
28+
"onDebugResolve:github-actions-job",
2829
"workspaceContains:**/.github/workflows/**",
2930
"workspaceContains:**/action.yml",
3031
"workspaceContains:**/action.yaml"
@@ -97,7 +98,19 @@
9798
}
9899
}
99100
},
101+
"debuggers": [
102+
{
103+
"type": "github-actions-job",
104+
"label": "GitHub Actions Job Debugger",
105+
"languages": []
106+
}
107+
],
100108
"commands": [
109+
{
110+
"command": "github-actions.debugger.connect",
111+
"category": "GitHub Actions",
112+
"title": "Connect to Actions Job Debugger..."
113+
},
101114
{
102115
"command": "github-actions.explorer.refresh",
103116
"category": "GitHub Actions",
@@ -544,6 +557,7 @@
544557
"@types/libsodium-wrappers": "^0.7.10",
545558
"@types/uuid": "^3.4.6",
546559
"@types/vscode": "^1.72.0",
560+
"@types/ws": "^8.18.1",
547561
"@typescript-eslint/eslint-plugin": "^5.40.0",
548562
"@typescript-eslint/parser": "^5.40.0",
549563
"@vscode/test-web": "^0.0.69",
@@ -579,7 +593,8 @@
579593
"tunnel": "0.0.6",
580594
"util": "^0.12.1",
581595
"uuid": "^3.3.3",
582-
"vscode-languageclient": "^8.0.2"
596+
"vscode-languageclient": "^8.0.2",
597+
"ws": "^8.20.0"
583598
},
584599
"overrides": {
585600
"browserify-sign": {

src/debugger/debugger.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as vscode from "vscode";
2+
import {newSession} from "../auth/auth";
3+
import {log, logError} from "../log";
4+
import {validateTunnelUrl} from "./tunnelUrl";
5+
import {WebSocketDapAdapter} from "./webSocketDapAdapter";
6+
7+
/** The custom debug type registered in package.json contributes.debuggers. */
8+
export const DEBUG_TYPE = "github-actions-job";
9+
10+
/**
11+
* Registers the Actions Job Debugger command and debug adapter factory.
12+
*
13+
* Contributes:
14+
* - A command-palette command that prompts for a tunnel URL and starts a debug session.
15+
* - A DebugAdapterDescriptorFactory that returns an inline DAP-over-WS adapter.
16+
*/
17+
export function registerDebugger(context: vscode.ExtensionContext): void {
18+
// Register the inline adapter factory for our debug type.
19+
context.subscriptions.push(
20+
vscode.debug.registerDebugAdapterDescriptorFactory(DEBUG_TYPE, new ActionsDebugAdapterFactory())
21+
);
22+
23+
// Register the connect command.
24+
context.subscriptions.push(
25+
vscode.commands.registerCommand("github-actions.debugger.connect", () => connectToDebugger())
26+
);
27+
}
28+
29+
// ── Connect command ──────────────────────────────────────────────────
30+
31+
async function connectToDebugger(): Promise<void> {
32+
// 1. Prompt for the tunnel URL.
33+
const rawUrl = await vscode.window.showInputBox({
34+
title: "Connect to Actions Job Debugger",
35+
prompt: "Enter the debugger tunnel URL (wss://…)",
36+
placeHolder: "wss://xxxx-4711.region.devtunnels.ms/",
37+
ignoreFocusOut: true,
38+
validateInput: input => {
39+
if (!input) {
40+
return "A tunnel URL is required";
41+
}
42+
const result = validateTunnelUrl(input);
43+
return result.valid ? null : result.reason;
44+
}
45+
});
46+
47+
if (!rawUrl) {
48+
return; // user cancelled
49+
}
50+
51+
const validation = validateTunnelUrl(rawUrl);
52+
if (!validation.valid) {
53+
void vscode.window.showErrorMessage(`Invalid tunnel URL: ${validation.reason}`);
54+
return;
55+
}
56+
57+
// 2. Acquire a GitHub auth session. The token is used as a Bearer token
58+
// against the Dev Tunnel relay, which accepts VS Code's GitHub app tokens.
59+
let session: vscode.AuthenticationSession;
60+
try {
61+
session = await newSession("Sign in to GitHub to connect to the Actions job debugger.");
62+
} catch (e) {
63+
void vscode.window.showErrorMessage(`GitHub authentication required: ${(e as Error).message}`);
64+
return;
65+
}
66+
67+
// 3. Launch the debug session. The factory will use the tunnel URL and token
68+
// from the configuration to create the websocket adapter.
69+
const config: vscode.DebugConfiguration = {
70+
type: DEBUG_TYPE,
71+
name: "Actions Job Debugger",
72+
request: "attach",
73+
tunnelUrl: validation.url,
74+
__token: session.accessToken
75+
};
76+
77+
log(`Starting debug session for ${validation.url}`);
78+
79+
const started = await vscode.debug.startDebugging(undefined, config);
80+
if (!started) {
81+
void vscode.window.showErrorMessage("Failed to start the debug session. Check the GitHub Actions output for details.");
82+
}
83+
}
84+
85+
// ── Debug adapter factory ────────────────────────────────────────────
86+
87+
class ActionsDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
88+
async createDebugAdapterDescriptor(
89+
session: vscode.DebugSession
90+
): Promise<vscode.DebugAdapterDescriptor> {
91+
const tunnelUrl = session.configuration.tunnelUrl as string | undefined;
92+
const token = session.configuration.__token as string | undefined;
93+
94+
if (!tunnelUrl || !token) {
95+
throw new Error(
96+
"Missing tunnel URL or authentication token. Use the 'Connect to Actions Job Debugger' command to start a session."
97+
);
98+
}
99+
100+
const adapter = new WebSocketDapAdapter(tunnelUrl, token);
101+
102+
try {
103+
await adapter.connect();
104+
} catch (e) {
105+
adapter.dispose();
106+
const msg = (e as Error).message;
107+
logError(e as Error, "Failed to connect debugger adapter");
108+
throw new Error(`Could not connect to the debugger tunnel: ${msg}`);
109+
}
110+
111+
return new vscode.DebugAdapterInlineImplementation(adapter);
112+
}
113+
}

src/debugger/tunnelUrl.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {validateTunnelUrl} from "./tunnelUrl";
2+
3+
describe("validateTunnelUrl", () => {
4+
it("accepts a valid wss:// URL", () => {
5+
const result = validateTunnelUrl("wss://abcdef-4711.uks1.devtunnels.ms/");
6+
expect(result).toEqual({valid: true, url: "wss://abcdef-4711.uks1.devtunnels.ms/"});
7+
});
8+
9+
it("accepts a valid ws:// URL", () => {
10+
const result = validateTunnelUrl("ws://abcdef-4711.uks1.devtunnels.ms/");
11+
expect(result).toEqual({valid: true, url: "ws://abcdef-4711.uks1.devtunnels.ms/"});
12+
});
13+
14+
it("accepts a URL without trailing slash", () => {
15+
const result = validateTunnelUrl("wss://abcdef-4711.uks1.devtunnels.ms");
16+
expect(result.valid).toBe(true);
17+
});
18+
19+
it("accepts a URL with a path", () => {
20+
const result = validateTunnelUrl("wss://abcdef-4711.uks1.devtunnels.ms/connect");
21+
expect(result.valid).toBe(true);
22+
});
23+
24+
it("rejects http:// scheme", () => {
25+
const result = validateTunnelUrl("http://abcdef-4711.uks1.devtunnels.ms/");
26+
expect(result.valid).toBe(false);
27+
if (!result.valid) {
28+
expect(result.reason).toContain("ws://");
29+
}
30+
});
31+
32+
it("rejects https:// scheme", () => {
33+
const result = validateTunnelUrl("https://abcdef-4711.uks1.devtunnels.ms/");
34+
expect(result.valid).toBe(false);
35+
if (!result.valid) {
36+
expect(result.reason).toContain("ws://");
37+
}
38+
});
39+
40+
it("rejects empty string", () => {
41+
const result = validateTunnelUrl("");
42+
expect(result.valid).toBe(false);
43+
});
44+
45+
it("rejects invalid URL format", () => {
46+
const result = validateTunnelUrl("not a url at all");
47+
expect(result.valid).toBe(false);
48+
if (!result.valid) {
49+
expect(result.reason).toContain("Invalid URL");
50+
}
51+
});
52+
53+
it("rejects URL with just a scheme", () => {
54+
const result = validateTunnelUrl("wss://");
55+
expect(result.valid).toBe(false);
56+
});
57+
});

src/debugger/tunnelUrl.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Validates a Dev Tunnel websocket URL for the Actions job debugger.
3+
*
4+
* Accepted formats:
5+
* wss://xxxx-4711.region.devtunnels.ms/
6+
* ws://xxxx-4711.region.devtunnels.ms/
7+
*
8+
* The URL must use a websocket scheme and include a non-empty host.
9+
*/
10+
export function validateTunnelUrl(raw: string): {valid: true; url: string} | {valid: false; reason: string} {
11+
let parsed: URL;
12+
try {
13+
parsed = new URL(raw);
14+
} catch {
15+
return {valid: false, reason: "Invalid URL format"};
16+
}
17+
18+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
19+
return {valid: false, reason: `URL must use ws:// or wss:// scheme, got "${parsed.protocol.replace(":", "")}://"`};
20+
}
21+
22+
if (!parsed.host) {
23+
return {valid: false, reason: "URL must include a host"};
24+
}
25+
26+
return {valid: true, url: parsed.toString()};
27+
}

0 commit comments

Comments
 (0)