Source: gitHubServer/entities/actions.js

/**
 * 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();
    }
  }
}