How to Use Salesforce LWC to Trigger GitHub Actions Workflows

A practical guide to triggering GitHub Actions workflows directly from Salesforce using Lightning Web Components (LWC) and Apex middleware — includes architecture, code samples, and best practices for secure automation.

Overview

This guide demonstrates how to connect Salesforce to GitHub Actions to trigger CI/CD workflows from within the CRM. The pattern uses a Custom Metadata Type for configuration, an Apex service class as middleware to talk to the GitHub API, and a reusable Lightning Web Component to provide a simple admin UI. Use this approach to centralize DevOps triggers, reduce context switching, and empower non-developer admins to start automated workflows safely.

Why this matters

Embedding GitHub workflow triggers in Salesforce helps organizations:

  • Reduce the friction of launching CI/CD jobs from business-facing interfaces.
  • Improve governance by storing defaults in Custom Metadata.
  • Enable secure, auditable initiation of automated processes without sharing developer credentials widely.

Architecture & components

Key components:

  • Custom Metadata Type (Github_Settings__mdt) to store defaults (owner, repo, workflow, branch, PAT).
  • Apex middleware (GitHubActionService) to call the GitHub Actions API securely from Salesforce.
  • Lightning Web Component (gitHubWorkflowTrigger) acting as the admin UI and orchestration layer.

Step-by-step

1. Set up a GitHub PAT (or GitHub App)

Generate a Personal Access Token with repo and workflow scopes (or prefer a GitHub App / fine-grained tokens for org use). Store a short expiry and rotate regularly according to your security policy.

2. Define your GitHub Workflow

Create the workflow file in your repository under .github/workflows. Ensure it exposes inputs if you need to pass parameters (such as ORG_ALIAS).

3. Create Custom Metadata Type

Create a Custom Metadata Type named Github_Settings__mdt with fields to store defaults:

  • Github_Branch__c (Text 255)
  • Github_Workflow__c (Text 255)
  • Github_Owner__c (Text 255)
  • Github_Repo__c (Text 255)
  • Github_PAT__c (Text 255) — consider storing an obfuscated value and restricting visibility

4. Apex middleware

Use an Apex class that reads Custom Metadata and calls the GitHub Actions API. Example (trimmed for brevity):

public class GitHubActionService {
    
    @AuraEnabled(cacheable=true)
    public static Map getDefaultGitHubSettings(String githubFlow) {
        Map settings = new Map();
        try {
            Github_Settings__mdt config = Github_Settings__mdt.getInstance(githubFlow);
            if (config != null) {
                settings.put('githubOwner', config.Github_Owner__c);
                settings.put('githubRepo', config.Github_Repo__c);
                settings.put('githubWorkflow', config.Github_Workflow__c);
                settings.put('githubBranch', config.Github_Branch__c);
                settings.put('githubPAT', config.Github_PAT__c);
            }
        } catch (Exception e) {
            throw new AuraHandledException('Error retrieving GitHub settings: ' + e.getMessage());
        }
        return settings;
    }
    
    @AuraEnabled
    public static String triggerWorkflow(
        String githubOwner,
        String githubRepo,
        String githubWorkflow,
        String githubBranch,
        String githubPat,
        String orgAlias
    ) {
        try {
            Http http = new Http();
            HttpRequest request = new HttpRequest();

            request.setEndpoint('https://api.github.com/repos/' + githubOwner + '/' + githubRepo + '/actions/workflows/' + githubWorkflow + '/dispatches');
            request.setMethod('POST');

            request.setHeader('Accept', 'application/vnd.github+json');
            request.setHeader('X-GitHub-Api-Version', '2022-11-28');
            request.setHeader('Authorization', 'Bearer ' + githubPat);
            request.setHeader('Content-Type', 'application/json');

            Map payload = new Map();
            payload.put('ref', githubBranch);

            Map inputs = new Map();
            inputs.put('ORG_ALIAS', orgAlias);

            payload.put('inputs', inputs);

            String body = JSON.serialize(payload);
            request.setBody(body);

            HttpResponse response = http.send(request);

            if (response.getStatusCode() == 204) {
                return 'SUCCESS: GitHub workflow triggered successfully.';
            } else {
                return 'ERROR: ' + response.getStatus() + ' - ' + response.getBody();
            }
        } catch (Exception e) {
            return 'ERROR: ' + e.getMessage();
        }
    }
}

5. Lightning Web Component

Build a simple LWC admin panel to load defaults, validate inputs, and call the Apex method. Key files include the JS, HTML and meta XML for exposing targets.

// gitHubWorkflowTrigger.js
import { LightningElement, track, api, wire } from 'lwc';
import triggerWorkflow from '@salesforce/apex/GitHubActionService.triggerWorkflow';
import getDefaultGitHubSettings from '@salesforce/apex/GitHubActionService.getDefaultGitHubSettings';

export default class GitHubWorkflowTrigger extends LightningElement {
   @track githubOwner = '';
   @track githubRepo = '';
   @track githubWorkflow = '';
   @track githubBranch = '';
   @track githubPat = '';
   @track resultMessage = '';
   @track isLoading = false;
   @track settingsLoaded = false;
   @api orgAlias = 'orgAlias';
   @api githubFlow = 'Run_LWC_Flow';

   @wire(getDefaultGitHubSettings , { githubFlow: '$githubFlow' })
   wiredSettings({ error, data }) {
       if (data) {
           this.githubOwner = data.githubOwner || '';
           this.githubRepo = data.githubRepo || '';
           this.githubWorkflow = data.githubWorkflow || '';
           this.githubBranch = data.githubBranch || '';
           this.githubPat = data.githubPAT || '';
           this.settingsLoaded = true;
       } else if (error) {
           this.resultMessage = 'Error loading default settings: ' + (error.body?.message || error.message);
       }
   }

   handleInputChange(event) {
       const field = event.target.name;
       this[field] = event.target.value;
   }

   async handleTriggerClick() {
       if (this.isLoading || !this.settingsLoaded) return;
       if (!this.githubOwner || !this.githubRepo || !this.githubWorkflow || !this.githubBranch || !this.githubPat) {
           this.resultMessage = 'ERROR: Please fill in all required GitHub fields';
           return;
       }

       this.isLoading = true;
       this.resultMessage = '';

       try {
           const result = await triggerWorkflow({
                githubOwner: this.githubOwner,
                githubRepo: this.githubRepo,
                githubWorkflow: this.githubWorkflow,
                githubBranch: this.githubBranch,
                githubPat: this.githubPat,
                orgAlias: this.orgAlias
           });

           this.resultMessage = result;
       } catch (error) {
           this.resultMessage = 'ERROR: ' + error.body.message;
       } finally {
           this.isLoading = false;
       }
   }
}

Integration and testing

  • Expose the LWC on Experience Builder (or App, Home, Record pages) and configure properties (orgAlias, githubFlow).
  • Publish the Experience site and trigger the component.
  • Confirm the workflow execution in the repository Actions tab.

Security and best practices

  • Avoid storing long-lived PATs; prefer GitHub Apps or fine-grained tokens for organizations.
  • Restrict access to Custom Metadata and component visibility via profiles/permission sets.
  • Log actions and responses for auditability and monitoring.

Conclusion

This pattern turns Salesforce into a lightweight DevOps orchestrator — empowering admins and reducing developer friction for CI/CD tasks. Test thoroughly in sandboxes and adapt authentication to your security standards before deploying to production.

Assigned category: DevOps