11import * as crypto from "crypto" ;
22import * as vscode from "vscode" ;
3+ import { getClient } from "../api/api" ;
34import { getSession , newSession } from "../auth/auth" ;
5+ import { getGitHubApiUri } from "../configuration/configuration" ;
46import { log , logDebug , logError } from "../log" ;
7+ import { parseJobUrl } from "./jobUrl" ;
58import { validateTunnelUrl } from "./tunnelUrl" ;
69import { WebSocketDapAdapter } from "./webSocketDapAdapter" ;
710
8- /** The custom debug type registered in package.json contributes.debuggers. */
911export const DEBUG_TYPE = "github-actions-job" ;
1012
1113/**
12- * Extension-private store for auth tokens, keyed by a one-time session
13- * nonce. Tokens are never placed in DebugConfiguration (which is readable
14- * by other extensions via vscode.debug.activeDebugSession.configuration).
14+ * Extension-private token store keyed by one-time nonce. Tokens are never
15+ * placed in DebugConfiguration (readable by other extensions).
1516 */
1617const pendingTokens = new Map < string , string > ( ) ;
1718
18- /**
19- * Registers the Actions Job Debugger command and debug adapter factory.
20- *
21- * Contributes:
22- * - A command-palette command that prompts for a tunnel URL and starts a debug session.
23- * - A DebugAdapterDescriptorFactory that returns an inline DAP-over-WS adapter.
24- */
2519export function registerDebugger ( context : vscode . ExtensionContext ) : void {
26- // Register the inline adapter factory for our debug type.
2720 context . subscriptions . push (
2821 vscode . debug . registerDebugAdapterDescriptorFactory ( DEBUG_TYPE , new ActionsDebugAdapterFactory ( ) )
2922 ) ;
3023
31- // Register a tracker to log all DAP traffic for diagnostics.
3224 context . subscriptions . push (
3325 vscode . debug . registerDebugAdapterTrackerFactory ( DEBUG_TYPE , new ActionsDebugTrackerFactory ( ) )
3426 ) ;
3527
36- // Register the connect command.
3728 context . subscriptions . push (
3829 vscode . commands . registerCommand ( "github-actions.debugger.connect" , ( ) => connectToDebugger ( ) )
3930 ) ;
4031}
4132
4233async function connectToDebugger ( ) : Promise < void > {
43- // 1. Prompt for the tunnel URL.
4434 const rawUrl = await vscode . window . showInputBox ( {
4535 title : "Connect to Actions Job Debugger" ,
46- prompt : "Enter the debugger tunnel URL (wss://…) " ,
47- placeHolder : "wss ://xxxx-4711.region.devtunnels.ms/ " ,
36+ prompt : "Paste the URL of the Actions job to debug " ,
37+ placeHolder : "https ://github.com/owner/repo/actions/runs/123/job/456 " ,
4838 ignoreFocusOut : true ,
4939 validateInput : input => {
5040 if ( ! input ) {
51- return "A tunnel URL is required" ;
41+ return "A job URL is required" ;
5242 }
53- const result = validateTunnelUrl ( input ) ;
43+ const result = parseJobUrl ( input , getGitHubApiUri ( ) ) ;
5444 return result . valid ? null : result . reason ;
5545 }
5646 } ) ;
5747
5848 if ( ! rawUrl ) {
59- return ; // user cancelled
49+ return ;
6050 }
6151
62- const validation = validateTunnelUrl ( rawUrl ) ;
63- if ( ! validation . valid ) {
64- void vscode . window . showErrorMessage ( `Invalid tunnel URL: ${ validation . reason } ` ) ;
52+ const parsed = parseJobUrl ( rawUrl , getGitHubApiUri ( ) ) ;
53+ if ( ! parsed . valid ) {
54+ void vscode . window . showErrorMessage ( `Invalid job URL: ${ parsed . reason } ` ) ;
6555 return ;
6656 }
6757
68- // 2. Acquire a GitHub auth session. The token is used as a Bearer token
69- // against the Dev Tunnel relay, which accepts VS Code's GitHub app tokens.
70- // Try silently first; fall back to prompting for sign-in if needed.
58+ // Try silently first; fall back to prompting for sign-in if needed.
7159 let session = await getSession ( ) ;
7260 if ( ! session ) {
7361 try {
@@ -80,10 +68,48 @@ async function connectToDebugger(): Promise<void> {
8068 }
8169 }
8270
83- // 3. Launch the debug session. The token is stored in extension-private
84- // memory (not in the configuration) to avoid exposing it to other extensions.
71+ const token = session . accessToken ;
72+ let debuggerUrl : string ;
73+ try {
74+ debuggerUrl = await vscode . window . withProgress (
75+ { location : vscode . ProgressLocation . Notification , title : "Connecting to Actions job debugger…" } ,
76+ async ( ) => {
77+ const octokit = getClient ( token ) ;
78+ const response = await octokit . request ( "GET /repos/{owner}/{repo}/actions/jobs/{job_id}/debugger" , {
79+ owner : parsed . owner ,
80+ repo : parsed . repo ,
81+ job_id : parsed . jobId
82+ } ) ;
83+ return ( response . data as { debugger_url : string } ) . debugger_url ;
84+ }
85+ ) ;
86+ } catch ( e ) {
87+ const status = ( e as { status ?: number } ) . status ;
88+ if ( status === 404 ) {
89+ void vscode . window . showErrorMessage (
90+ "Debugger is not available for this job. Make sure the job is running with debugging enabled."
91+ ) ;
92+ } else if ( status === 403 ) {
93+ void vscode . window . showErrorMessage (
94+ "Permission denied. You may need to re-authenticate or check your access to this repository."
95+ ) ;
96+ } else {
97+ const msg = ( e as Error ) . message || "Unknown error" ;
98+ void vscode . window . showErrorMessage ( `Failed to fetch debugger URL: ${ msg } ` ) ;
99+ }
100+ return ;
101+ }
102+
103+ const validation = validateTunnelUrl ( debuggerUrl ) ;
104+ if ( ! validation . valid ) {
105+ void vscode . window . showErrorMessage ( `Invalid debugger URL returned by API: ${ validation . reason } ` ) ;
106+ return ;
107+ }
108+
109+ // Store token in extension-private memory (not in the config) to avoid
110+ // exposing it to other extensions.
85111 const nonce = crypto . randomBytes ( 16 ) . toString ( "hex" ) ;
86- pendingTokens . set ( nonce , session . accessToken ) ;
112+ pendingTokens . set ( nonce , token ) ;
87113
88114 const config : vscode . DebugConfiguration = {
89115 type : DEBUG_TYPE ,
@@ -114,7 +140,7 @@ class ActionsDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
114140 const nonce = session . configuration . __tokenNonce as string | undefined ;
115141 const token = nonce ? pendingTokens . get ( nonce ) : undefined ;
116142
117- // Consume the token immediately so it cannot be replayed.
143+ // Consume immediately so it cannot be replayed.
118144 if ( nonce ) {
119145 pendingTokens . delete ( nonce ) ;
120146 }
@@ -125,7 +151,6 @@ class ActionsDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
125151 ) ;
126152 }
127153
128- // Re-validate the tunnel URL as defense-in-depth
129154 const revalidation = validateTunnelUrl ( tunnelUrl ) ;
130155 if ( ! revalidation . valid ) {
131156 throw new Error ( `Invalid debugger tunnel URL: ${ revalidation . reason } ` ) ;
0 commit comments