Skip to content
Draft
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
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"activationEvents": [
"onView:workflows",
"onView:settings",
"onView:github-actions.workflow-debug",
"workspaceContains:**/.github/workflows/**",
"workspaceContains:**/action.yml",
"workspaceContains:**/action.yaml"
Expand Down Expand Up @@ -63,6 +64,21 @@
]
}
],
"breakpoints": [
{
"language": "github-actions-workflow"
}
],
"debuggers": [
{
"type": "github-actions",
"label": "GitHub Actions",
"languages": [
"github-actions-workflow"
],
"initialConfigurations": []
}
],
"configuration": {
"title": "GitHub Actions",
"properties": {
Expand Down Expand Up @@ -155,6 +171,20 @@
"light": "resources/icons/light/logs.svg"
}
},
{
"command": "github-actions.workflow.job.attachDebugger",
"category": "GitHub Actions",
"title": "Attach debugger to job",
"when": "viewItem =~ /job/ && viewItem =~ /running/",
"icon": "$(debug)"
},
{
"command": "github-actions.workflow.job.rerunDebug",
"category": "GitHub Actions",
"title": "Re-run and debug job",
"when": "viewItem =~ /job/ && viewItem =~ /failed/",
"icon": "$(debug)"
},
{
"command": "github-actions.step.logs",
"category": "GitHub Actions",
Expand Down Expand Up @@ -267,6 +297,13 @@
}
],
"views": {
"debug": [
{
"id": "github-actions.workflow-debug",
"name": "Actions Remote File System",
"when": "github-actions.debugging"
}
],
"github-actions": [
{
"id": "github-actions.current-branch",
Expand Down Expand Up @@ -373,6 +410,16 @@
"when": "viewItem =~ /run\\s/",
"group": "inline"
},
{
"command": "github-actions.workflow.job.attachDebugger",
"group": "inline@0",
"when": "viewItem =~ /job/ && viewItem =~ /running/"
},
{
"command": "github-actions.workflow.job.rerunDebug",
"group": "inline@1",
"when": "viewItem =~ /job/ && viewItem =~ /failed/"
},
{
"command": "github-actions.workflow.logs",
"group": "inline",
Expand Down Expand Up @@ -458,6 +505,14 @@
"command": "github-actions.workflow.logs",
"when": "false"
},
{
"command": "github-actions.workflow.job.attachDebugger",
"when": "false"
},
{
"command": "github-actions.workflow.job.rerunDebug",
"when": "false"
},
{
"command": "github-actions.step.logs",
"when": "false"
Expand Down Expand Up @@ -566,6 +621,7 @@
"@actions/languageserver": "^0.3.46",
"@actions/workflow-parser": "^0.3.46",
"@octokit/rest": "^21.1.1",
"@vscode/debugprotocol": "^1.68.0",
"@vscode/vsce": "^2.19.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
Expand All @@ -589,4 +645,4 @@
"elliptic": "6.6.1"
}
}
}
}
43 changes: 43 additions & 0 deletions src/commands/attachWorkflowJobDebugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as vscode from "vscode";
import {WorkflowJobNode} from "../treeViews/shared/workflowJobNode";
import {getGitHubContext} from "../git/repository";

export type AttachWorkflowJobDebuggerArgs = Pick<WorkflowJobNode, "gitHubRepoContext" | "job">;

export function registerAttachWorkflowJobDebugger(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand(
"github-actions.workflow.job.attachDebugger",
async (args: AttachWorkflowJobDebuggerArgs) => {
const job = args.job.job;
const repoContext = args.gitHubRepoContext;
const workflowName = job.workflow_name || undefined;
const jobName = job.name;
const title = workflowName ? `Workflow "${workflowName}" job "${jobName}"` : `Job "${jobName}"`;

// Get current GitHub user
const gitHubContext = await getGitHubContext();
const username = gitHubContext?.username || "unknown";

const debugConfig: vscode.DebugConfiguration = {
name: `GitHub Actions: ${title}`,
type: "github-actions",
request: "attach",
workflowName,
jobName,
// Identity fields for DAP proxy audit logging
githubActor: username,
githubRepository: `${repoContext.owner}/${repoContext.name}`,
githubRunID: String(job.run_id),
githubJobID: String(job.id)
};

const folder = vscode.workspace.workspaceFolders?.[0];
const started = await vscode.debug.startDebugging(folder, debugConfig);
if (!started) {
await vscode.window.showErrorMessage("Failed to start GitHub Actions debug session.");
}
}
)
);
}
133 changes: 133 additions & 0 deletions src/commands/rerunWorkflowJobDebug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as vscode from "vscode";

import {WorkflowJob as WorkflowJobModel} from "../model";
import {WorkflowJob} from "../store/WorkflowJob";
import {WorkflowJobCommandArgs, WorkflowJobNode} from "../treeViews/shared/workflowJobNode";

export function registerReRunWorkflowJobWithDebug(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("github-actions.workflow.job.rerunDebug", async (args: WorkflowJobCommandArgs) => {
const gitHubRepoContext = args.gitHubRepoContext;
const job = args.job;
const jobId = job.job.id;
const runId = job.job.run_id;
const jobName = job.job.name;

if (!jobId) {
await vscode.window.showErrorMessage("Unable to re-run workflow job: missing job id.");
return;
}

if (!runId) {
await vscode.window.showErrorMessage("Unable to re-run workflow job: missing run id.");
return;
}

try {
await gitHubRepoContext.client.request("POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun", {
owner: gitHubRepoContext.owner,
repo: gitHubRepoContext.name,
job_id: jobId,
enable_debug_logging: true
});
} catch (e) {
await vscode.window.showErrorMessage(
`Could not re-run workflow job with debug logging: '${(e as Error).message}'`
);
return;
}

WorkflowJobNode.setStatusOverride(runId, jobName, "pending", null);
await refreshWorkflowViews();

const updatedJob = await pollJobRunning(gitHubRepoContext, runId, jobName, 15, 1000);
if (!updatedJob) {
await vscode.window.showWarningMessage("Job did not start running within 15 seconds.");
return;
}

await vscode.commands.executeCommand("github-actions.workflow.job.attachDebugger", {
gitHubRepoContext,
job: new WorkflowJob(gitHubRepoContext, updatedJob)
});
})
);
}

async function pollJobRunning(
gitHubRepoContext: WorkflowJobCommandArgs["gitHubRepoContext"],
runId: number,
jobName: string,
attempts: number,
delayMs: number
): Promise<WorkflowJobModel | undefined> {
const rerunStart = Date.now();
for (let attempt = 0; attempt < attempts; attempt++) {
const job = await getJobByName(gitHubRepoContext, runId, jobName, rerunStart);
if (job?.status === "in_progress") {
await clearStatusOverride(runId, jobName);
return job;
}

await delay(delayMs);
}

await clearStatusOverride(runId, jobName);
return undefined;
}

async function getJobByName(
gitHubRepoContext: WorkflowJobCommandArgs["gitHubRepoContext"],
runId: number,
jobName: string,
rerunStart: number
): Promise<WorkflowJobModel | undefined> {
try {
const response = await gitHubRepoContext.client.actions.listJobsForWorkflowRun({
owner: gitHubRepoContext.owner,
repo: gitHubRepoContext.name,
run_id: runId,
per_page: 100
});

const jobs = response.data.jobs ?? [];
const matching = jobs.filter(job => job.name === jobName);
if (matching.length === 0) {
return undefined;
}

const sorted = matching.sort((left, right) => {
const leftStart = left.started_at ? Date.parse(left.started_at) : 0;
const rightStart = right.started_at ? Date.parse(right.started_at) : 0;
return rightStart - leftStart;
});

const newest = sorted[0];
if (newest.started_at) {
const startedAt = Date.parse(newest.started_at);
if (!Number.isNaN(startedAt) && startedAt < rerunStart - 5000) {
return undefined;
}
}

return newest;
} catch {
return undefined;
}
}

async function refreshWorkflowViews(): Promise<void> {
await Promise.all([
vscode.commands.executeCommand("github-actions.explorer.refresh"),
vscode.commands.executeCommand("github-actions.explorer.current-branch.refresh")
]);
}

async function clearStatusOverride(runId: number, jobName: string): Promise<void> {
WorkflowJobNode.clearStatusOverride(runId, jobName);
await refreshWorkflowViews();
}

function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Loading
Loading