/**
* Actions entity class for GitHub workflow operations
* @file actions.js
* @license Apache-2.0
* @version 3.0.0
*
* @author Michael Hay <michael.hay@mediumroast.io>
* @copyright 2025 Mediumroast, Inc. All rights reserved.
*/
import { BaseObjects } from '../baseObjects.js';
import { logger } from '../logger.js';
export class Actions extends BaseObjects {
/**
* @constructor
* @param {string} token - GitHub API token
* @param {string} org - GitHub organization name
* @param {string} processName - Process name for locking
*/
constructor(token, org, processName) {
super(token, org, processName, 'Actions');
// Add actions-specific cache keys
this._cacheKeys.workflowRuns = 'workflow_runs';
this._cacheKeys.actionsBilling = 'actions_billing';
// Set specific cache timeouts
this.cacheTimeouts.workflowRuns = 60000; // 1 minute for workflow runs (dynamic data)
this.cacheTimeouts.actionsBilling = 3600000; // 1 hour for billing info
}
/**
* Update GitHub Actions workflow files
* @returns {Promise<Array>} Operation result
*/
async updateActions() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'updateActions') :
{ end: () => {} };
try {
return await this._executeTransaction([
// Step 1: Get action manifest
async () => {
try {
const manifestResp = await this.serverCtl.getActionsManifest();
if (!manifestResp[0]) {
return manifestResp;
}
// Store for next step
this._tempManifest = manifestResp[2];
return this._createSuccess('Retrieved actions manifest');
} catch (err) {
logger.error('Failed to retrieve actions manifest', err);
return this._createError(
`Failed to retrieve actions manifest: ${err.message}`,
err,
500
);
}
},
// Step 2: Install or update each action
async () => {
const installStatus = [];
for (const action of this._tempManifest) {
try {
// Check if action exists
const actionExists = await this.serverCtl.actionExists(action.name);
let result;
if (actionExists[0] && actionExists[2]) {
// Update existing action
result = await this.serverCtl.updateAction(
action.name,
action.content,
actionExists[2] // SHA
);
} else {
// Create new action
result = await this.serverCtl.createAction(
action.name,
action.content
);
}
// Add to status with operation type
installStatus.push({
name: action.name,
operation: actionExists[0] && actionExists[2] ? 'updated' : 'created',
success: result[0],
message: result[1],
timestamp: new Date().toISOString()
});
} catch (err) {
logger.error(`Failed to install action [${action.name}]`, err);
installStatus.push({
name: action.name,
operation: 'failed',
success: false,
message: err.message,
timestamp: new Date().toISOString()
});
}
}
// If all installations failed, return error
if (installStatus.every(status => !status.success)) {
return this._createError(
'All action installations failed',
installStatus,
500
);
}
return this._createSuccess(
`Actions installation completed: ${installStatus.filter(s => s.success).length} succeeded, ${installStatus.filter(s => !s.success).length} failed`,
installStatus
);
}
], 'update-actions');
} finally {
tracking.end();
}
}
/**
* Get actions billing information
* @returns {Promise<Array>} Billing information
*/
async getActionsBilling() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getActionsBilling') :
{ end: () => {} };
try {
// Use the standardized cache key structure
return await this.cache.getOrFetch(
this._cacheKeys.actionsBilling,
async () => this.serverCtl.getActionsBillings(),
this.cacheTimeouts.actionsBilling || 60000,
[] // No dependencies
);
} catch (error) {
return this._createError(
`Failed to retrieve Actions billing: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Get all workflow runs
* @returns {Promise<Array>} List of workflow runs
*/
async getAll() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getAll') :
{ end: () => {} };
try {
return await this.cache.getOrFetch(
this._cacheKeys.workflowRuns,
async () => {
try {
// Try the original implementation first
return await this.serverCtl.getWorkflowRuns();
} catch (error) {
// If the error is specifically about missing the method, use a fallback
if (error.message && error.message.includes('getWorkflowRuns is not a function')) {
logger.warn('getWorkflowRuns not implemented in github.js, using fallback implementation');
// Fallback implementation - returns an empty successful response
return [
true,
'Workflow runs functionality not fully implemented',
{
workflow_runs: [],
total_count: 0,
message: 'This is a placeholder. The getWorkflowRuns method needs to be implemented in the github.js file.'
}
];
}
// If it's another error, rethrow it
throw error;
}
},
this.cacheTimeouts.workflowRuns || 60000,
[] // No dependencies
);
} catch (error) {
return this._createError(
`Failed to retrieve workflow runs: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Get details for a specific workflow run
* @param {string} runId - Workflow run ID
* @returns {Promise<Array>} Workflow run details
*/
async getWorkflowRun(runId) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getWorkflowRun') :
{ end: () => {} };
try {
// Use standardized parameter validation
const validationError = this._validateParams(
{ runId },
{ runId: 'string' }
);
if (validationError) return validationError;
// Use cache for individual runs with dependency on all runs
const runCacheKey = `${this._cacheKeys.workflowRuns}_${runId}`;
return await this.cache.getOrFetch(
runCacheKey,
async () => this.serverCtl.getWorkflowRun(runId),
this.cacheTimeouts.workflowRuns || 60000,
[this._cacheKeys.workflowRuns] // Depends on all workflow runs
);
} catch (error) {
return this._createError(
`Failed to retrieve workflow run: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Cancel a workflow run
* @param {string} runId - Workflow run ID to cancel
* @returns {Promise<Array>} Result of operation
*/
async cancelWorkflowRun(runId) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'cancelWorkflowRun') :
{ end: () => {} };
try {
// Use standardized parameter validation
const validationError = this._validateParams(
{ runId },
{ runId: 'string' }
);
if (validationError) return validationError;
const result = await this.serverCtl.cancelWorkflowRun(runId);
// Invalidate cache on successful cancellation
if (result[0]) {
// Invalidate both the specific run and the list of all runs
this.cache.invalidate(this._cacheKeys.workflowRuns);
this.cache.invalidate(`${this._cacheKeys.workflowRuns}_${runId}`);
}
return result;
} catch (error) {
return this._createError(
`Failed to cancel workflow run: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Trigger a specific workflow
* @param {string} workflowId - Workflow file name (e.g., "main.yml")
* @param {Object} inputs - Workflow inputs
* @returns {Promise<Array>} Result of operation
*/
async triggerWorkflow(workflowId, inputs = {}) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'triggerWorkflow') :
{ end: () => {} };
try {
// Use standardized parameter validation
const validationError = this._validateParams(
{ workflowId, inputs },
{ workflowId: 'string', inputs: 'object' }
);
if (validationError) return validationError;
const result = await this.serverCtl.dispatchWorkflow(workflowId, inputs);
// Invalidate cache on successful trigger
if (result[0]) {
this.cache.invalidate(this._cacheKeys.workflowRuns);
}
return result;
} catch (error) {
return this._createError(
`Failed to trigger workflow: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Get usage metrics for GitHub Actions
* @returns {Promise<Array>} Actions usage metrics
*/
async getUsageMetrics() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getUsageMetrics') :
{ end: () => {} };
try {
// Use the standardized cache key structure for metrics
return await this.cache.getOrFetch(
this._cacheKeys.metrics,
async () => {
// Get billing information
const billingResp = await this.getActionsBilling();
if (!billingResp[0]) {
return billingResp;
}
// Get recent workflow runs
const runsResp = await this.getAll();
if (!runsResp[0]) {
return runsResp;
}
// Calculate metrics from the data
const billing = billingResp[2];
const runs = runsResp[2];
// Count runs by status
const statusCounts = {};
const workflowCounts = {};
runs.forEach(run => {
// Count by status
statusCounts[run.status] = (statusCounts[run.status] || 0) + 1;
// Count by workflow
const workflowName = run.workflow_id || 'unknown';
workflowCounts[workflowName] = (workflowCounts[workflowName] || 0) + 1;
});
// Build usage metrics
const metrics = {
billing: {
included_minutes: billing.included_minutes,
total_minutes_used: billing.total_minutes_used,
minutes_used_breakdown: billing.minutes_used_breakdown,
remaining_minutes: Math.max(0, billing.included_minutes - billing.total_minutes_used)
},
runs: {
total: runs.length,
by_status: statusCounts,
by_workflow: workflowCounts
},
period: {
start: billing.billing_period?.start_date,
end: billing.billing_period?.end_date
}
};
return this._createSuccess(
'Actions usage metrics compiled successfully',
metrics
);
},
this.cacheTimeouts.metrics || 300000,
[
this._cacheKeys.actionsBilling, // Metrics depend on billing data
this._cacheKeys.workflowRuns // Metrics depend on workflow runs data
]
);
} catch (error) {
return this._createError(
`Failed to get Actions usage metrics: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
}