450 lines
14 KiB
Handlebars
450 lines
14 KiB
Handlebars
{{template "base/head" .}}
|
|
<div class="page-content repository actions debug-page">
|
|
{{template "repo/header" .}}
|
|
<div class="ui container">
|
|
{{template "base/alert" .}}
|
|
|
|
<div class="ui stackable grid">
|
|
<!-- Left Panel: Workflow Editor -->
|
|
<div class="eight wide column">
|
|
<div class="ui segment">
|
|
<h3 class="ui header">
|
|
{{ctx.Locale.Tr "actions.workflow.debug"}}
|
|
<span class="ui grey text">({{.WorkflowID}})</span>
|
|
</h3>
|
|
|
|
<div class="ui form">
|
|
<!-- Workflow Editor -->
|
|
<div class="field">
|
|
<label>{{ctx.Locale.Tr "actions.workflow.content"}}</label>
|
|
<textarea id="workflowEditor" class="ui textarea" style="height: 400px; font-family: monospace;" placeholder="Workflow YAML content"></textarea>
|
|
</div>
|
|
|
|
<!-- Debug Parameters -->
|
|
<div class="two fields">
|
|
<div class="field">
|
|
<label>{{ctx.Locale.Tr "actions.workflow.ref"}}</label>
|
|
<select id="refSelect" class="ui dropdown">
|
|
<option value="">{{ctx.Locale.Tr "repo.default_branch"}}</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ctx.Locale.Tr "actions.workflow.event"}}</label>
|
|
<select id="eventSelect" class="ui dropdown">
|
|
<option value="push">push</option>
|
|
<option value="pull_request">pull_request</option>
|
|
<option value="workflow_dispatch">workflow_dispatch</option>
|
|
<option value="issues">issues</option>
|
|
<option value="issue_comment">issue_comment</option>
|
|
<option value="pull_request_review">pull_request_review</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Inputs -->
|
|
<div class="field">
|
|
<label>{{ctx.Locale.Tr "actions.workflow.inputs"}}</label>
|
|
<div id="inputsContainer" class="ui segment"></div>
|
|
<button type="button" class="ui mini button" id="addInputBtn">
|
|
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Environment Variables -->
|
|
<div class="field">
|
|
<label>{{ctx.Locale.Tr "actions.workflow.env"}}</label>
|
|
<div id="envContainer" class="ui segment"></div>
|
|
<button type="button" class="ui mini button" id="addEnvBtn">
|
|
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="ui form-actions">
|
|
<button type="button" class="ui primary button" id="runBtn" onclick="runDebugWorkflow()">
|
|
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.workflow.run"}}
|
|
</button>
|
|
<a href="{{$.ActionsURL}}" class="ui button">{{ctx.Locale.Tr "cancel"}}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel: Logs and Status -->
|
|
<div class="eight wide column">
|
|
<div class="ui segment">
|
|
<h3 class="ui header">
|
|
{{ctx.Locale.Tr "actions.workflow.logs"}}
|
|
</h3>
|
|
|
|
<!-- Status Info -->
|
|
<div id="statusInfo" style="display: none;" class="ui info message">
|
|
<p>{{ctx.Locale.Tr "actions.workflow.running"}}: <a id="runLink" href="#" target="_blank"></a></p>
|
|
</div>
|
|
|
|
<!-- Logs Container -->
|
|
<div id="logsContainer" style="border: 1px solid #ddd; padding: 10px; height: 500px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace; font-size: 12px;">
|
|
<p style="color: #999;">{{ctx.Locale.Tr "actions.workflow.logs_placeholder"}}</p>
|
|
</div>
|
|
|
|
<!-- Refresh and Copy Buttons -->
|
|
<div style="margin-top: 10px;">
|
|
<button type="button" class="ui mini button" id="refreshBtn" onclick="refreshLogs()" style="display: none;">
|
|
{{svg "octicon-sync"}} {{ctx.Locale.Tr "refresh"}}
|
|
</button>
|
|
<button type="button" class="ui mini button" id="copyBtn" onclick="copyLogs()" style="display: none;">
|
|
{{svg "octicon-copy"}} {{ctx.Locale.Tr "copy"}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Messages -->
|
|
<div id="errorMessage" style="display: none;" class="ui error message">
|
|
<p id="errorText"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const debugSessionID = {{.DebugSessionID}};
|
|
const workflowID = "{{.WorkflowID}}";
|
|
const defaultBranch = "{{.DefaultBranch}}";
|
|
const actionsURL = "{{.ActionsURL}}";
|
|
const debugAPIURL = "{{.DebugAPIURL}}";
|
|
const csrfToken = "{{.CsrfToken}}";
|
|
|
|
let currentRunIndex = null;
|
|
let logsAutoRefreshInterval = null;
|
|
|
|
// Initialize editor with workflow content
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const editor = document.getElementById('workflowEditor');
|
|
editor.value = `{{.WorkflowContent}}`;
|
|
|
|
// Initialize dropdowns
|
|
$('.ui.dropdown').dropdown();
|
|
|
|
// Populate ref select with branches and tags
|
|
populateRefs();
|
|
|
|
// Setup input/env add buttons
|
|
document.getElementById('addInputBtn').addEventListener('click', addInputField);
|
|
document.getElementById('addEnvBtn').addEventListener('click', addEnvField);
|
|
});
|
|
|
|
function populateRefs() {
|
|
const refSelect = document.getElementById('refSelect');
|
|
// Add default branch
|
|
const option = document.createElement('option');
|
|
option.value = defaultBranch;
|
|
option.text = defaultBranch + ' (default)';
|
|
refSelect.appendChild(option);
|
|
// TODO: fetch branches and tags from API
|
|
}
|
|
|
|
function addInputField() {
|
|
const container = document.getElementById('inputsContainer');
|
|
const id = 'input_' + Date.now();
|
|
const field = document.createElement('div');
|
|
field.className = 'ui form';
|
|
field.id = id;
|
|
field.innerHTML = `
|
|
<div class="two fields">
|
|
<div class="field">
|
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_name"}}" class="input-key">
|
|
</div>
|
|
<div class="field">
|
|
<div class="ui input">
|
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_value"}}" class="input-value">
|
|
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
|
|
{{svg "octicon-trash"}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(field);
|
|
}
|
|
|
|
function addEnvField() {
|
|
const container = document.getElementById('envContainer');
|
|
const id = 'env_' + Date.now();
|
|
const field = document.createElement('div');
|
|
field.className = 'ui form';
|
|
field.id = id;
|
|
field.innerHTML = `
|
|
<div class="two fields">
|
|
<div class="field">
|
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_name"}}" class="env-key">
|
|
</div>
|
|
<div class="field">
|
|
<div class="ui input">
|
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_value"}}" class="env-value">
|
|
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
|
|
{{svg "octicon-trash"}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(field);
|
|
}
|
|
|
|
function getInputsObject() {
|
|
const inputs = {};
|
|
document.querySelectorAll('#inputsContainer .input-key').forEach((key, index) => {
|
|
const keyElem = document.querySelectorAll('#inputsContainer .input-key')[index];
|
|
const valueElem = document.querySelectorAll('#inputsContainer .input-value')[index];
|
|
if (keyElem.value && valueElem.value) {
|
|
inputs[keyElem.value] = valueElem.value;
|
|
}
|
|
});
|
|
return inputs;
|
|
}
|
|
|
|
function getEnvObject() {
|
|
const env = {};
|
|
document.querySelectorAll('#envContainer .env-key').forEach((key, index) => {
|
|
const keyElem = document.querySelectorAll('#envContainer .env-key')[index];
|
|
const valueElem = document.querySelectorAll('#envContainer .env-value')[index];
|
|
if (keyElem.value && valueElem.value) {
|
|
env[keyElem.value] = valueElem.value;
|
|
}
|
|
});
|
|
return env;
|
|
}
|
|
|
|
function runDebugWorkflow() {
|
|
const workflowContent = document.getElementById('workflowEditor').value;
|
|
const ref = document.getElementById('refSelect').value || defaultBranch;
|
|
const event = document.getElementById('eventSelect').value;
|
|
const inputs = getInputsObject();
|
|
const env = getEnvObject();
|
|
|
|
if (!workflowContent) {
|
|
showError('{{ctx.Locale.Tr "actions.workflow.content_empty"}}');
|
|
return;
|
|
}
|
|
|
|
const runBtn = document.getElementById('runBtn');
|
|
runBtn.classList.add('loading');
|
|
runBtn.disabled = true;
|
|
|
|
const payload = {
|
|
workflow_content: workflowContent,
|
|
ref: ref,
|
|
event: event,
|
|
inputs: inputs,
|
|
env: env
|
|
};
|
|
|
|
console.log('Submitting to:', `${debugAPIURL}/${debugSessionID}/run`);
|
|
console.log('Payload:', payload);
|
|
console.log('CSRF Token:', csrfToken);
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Csrf-Token': csrfToken,
|
|
};
|
|
|
|
fetch(`${debugAPIURL}/${debugSessionID}/run`, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(payload),
|
|
})
|
|
.then(response => {
|
|
console.log('Response status:', response.status, 'OK:', response.ok);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
console.log('Error response:', text);
|
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Response data:', data);
|
|
if (data.success || data.run_index) {
|
|
currentRunIndex = data.run_index;
|
|
document.getElementById('statusInfo').style.display = 'block';
|
|
document.getElementById('runLink').href = `${actionsURL}/runs/${currentRunIndex}`;
|
|
document.getElementById('runLink').textContent = `Run #${currentRunIndex}`;
|
|
document.getElementById('errorMessage').style.display = 'none';
|
|
document.getElementById('refreshBtn').style.display = 'inline-block';
|
|
document.getElementById('copyBtn').style.display = 'inline-block';
|
|
|
|
// Start auto-refreshing logs
|
|
refreshLogs();
|
|
logsAutoRefreshInterval = setInterval(refreshLogs, 2000);
|
|
} else {
|
|
showError(data.error || data.message || '{{ctx.Locale.Tr "error"}}');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fetch error:', error);
|
|
showError('Failed to run workflow: ' + error.message);
|
|
})
|
|
.finally(() => {
|
|
runBtn.classList.remove('loading');
|
|
runBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
function refreshLogs() {
|
|
if (!currentRunIndex) return;
|
|
|
|
console.log('Refreshing logs for run index:', currentRunIndex);
|
|
|
|
// Get logs from the action run API via POST
|
|
const payload = {
|
|
LogCursors: []
|
|
};
|
|
|
|
fetch(`${actionsURL}/runs/${currentRunIndex}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Csrf-Token': csrfToken,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
})
|
|
.then(response => {
|
|
console.log('Log fetch response status:', response.status, response.statusText);
|
|
console.log('Content-Type:', response.headers.get('Content-Type'));
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
// Check if response is JSON
|
|
const contentType = response.headers.get('Content-Type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
return response.json();
|
|
} else {
|
|
// If not JSON, get text and try to parse
|
|
return response.text().then(text => {
|
|
console.log('Response is not JSON, attempting to parse');
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
console.error('Failed to parse response as JSON:', text.substring(0, 200));
|
|
throw new Error('Response is not valid JSON. Content: ' + text.substring(0, 500));
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.then(data => {
|
|
console.log('Log data received:', data);
|
|
const logsContainer = document.getElementById('logsContainer');
|
|
|
|
if (!data) {
|
|
logsContainer.textContent = 'No response data';
|
|
return;
|
|
}
|
|
|
|
if (data.logs && data.logs.stepsLog && data.logs.stepsLog.length > 0) {
|
|
let logContent = '';
|
|
data.logs.stepsLog.forEach((stepLog, index) => {
|
|
console.log(`Step ${index}:`, stepLog);
|
|
|
|
if (stepLog.rawOutput) {
|
|
logContent += stepLog.rawOutput + '\n';
|
|
} else if (stepLog.lines && Array.isArray(stepLog.lines)) {
|
|
stepLog.lines.forEach(line => {
|
|
if (line && line.content) {
|
|
logContent += line.content + '\n';
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (logContent.trim()) {
|
|
logsContainer.textContent = logContent;
|
|
} else {
|
|
logsContainer.textContent = 'Waiting for logs...';
|
|
}
|
|
} else if (data.state && data.state.run) {
|
|
// Show run status if logs not ready
|
|
logsContainer.textContent = 'Status: ' + (data.state.run.status || 'waiting') + '\n\nWaiting for logs...';
|
|
} else {
|
|
logsContainer.textContent = 'Waiting for logs...';
|
|
}
|
|
logsContainer.scrollTop = logsContainer.scrollHeight;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching logs:', error);
|
|
document.getElementById('logsContainer').textContent = 'Error: ' + error.message;
|
|
});
|
|
}
|
|
|
|
function copyLogs() {
|
|
const logsContainer = document.getElementById('logsContainer');
|
|
const text = logsContainer.textContent;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
// Show success message
|
|
const copyBtn = document.getElementById('copyBtn');
|
|
const originalText = copyBtn.innerHTML;
|
|
copyBtn.innerHTML = '{{svg "octicon-check"}} Copied!';
|
|
setTimeout(() => {
|
|
copyBtn.innerHTML = originalText;
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorMsg = document.getElementById('errorMessage');
|
|
document.getElementById('errorText').textContent = message;
|
|
errorMsg.style.display = 'block';
|
|
// Clear after 5 seconds
|
|
setTimeout(() => {
|
|
errorMsg.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', function() {
|
|
if (logsAutoRefreshInterval) {
|
|
clearInterval(logsAutoRefreshInterval);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.debug-page .ui.segment {
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
|
}
|
|
|
|
.debug-page #workflowEditor {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.debug-page #logsContainer {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.debug-page .ui.form-actions {
|
|
margin-top: 20px;
|
|
padding-top: 15px;
|
|
border-top: 1px solid rgba(34, 36, 38, 0.15);
|
|
}
|
|
|
|
.debug-page .ui.form-actions .button {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.debug-page .input-key,
|
|
.debug-page .input-value,
|
|
.debug-page .env-key,
|
|
.debug-page .env-value {
|
|
margin: 0;
|
|
}
|
|
</style>
|
|
|
|
{{template "base/footer" .}}
|