Building Custom IssueOps
Create custom IssueOps by building GitHub Actions workflows that respond to issues with structured data. No modifications to sfp required.
How It Works
User creates an issue with JSON payload (via DevCentral form or directly)
GitHub Actions workflow triggers on issue events
Workflow parses the JSON and executes custom logic
Workflow posts updates back to the issue
DevCentral Integration
Custom IssueOps can integrate with DevCentral in two ways:
For Built-in IssueOps: DevCentral automatically displays forms for standard operations (scratch orgs, packages, etc.) in the Service Catalogue.
For Custom IssueOps: Add form definitions to your DevCentral repository's _forms directory. These will appear in the Service Catalogue, allowing users to submit requests through a user-friendly interface instead of writing JSON.
Complete Example: Custom Deployment Workflow
Here's a working example of a custom IssueOps that deploys to an environment.
Note: This example shows how to create a completely custom IssueOps workflow. For standard operations like scratch org requests or package installations, use the built-in IssueOps provided by sfops.
1. Create the Issue Form (Optional)
For user-friendly forms, add this file to your DevCentral repository's _forms/custom-deploy.yml:
name: Custom Deployment Request
description: Deploy a specific version to an environment
title: "[DEPLOY] "
labels: ["deployment", "custom-ops"]
body:
- type: markdown
attributes:
value: |
## Deployment Request
Use this form to deploy a specific version to an environment.
- type: input
id: version
attributes:
label: Version
description: The release version to deploy
placeholder: "v1.2.3"
validations:
required: true
- type: dropdown
id: environment
attributes:
label: Target Environment
description: Select the environment to deploy to
options:
- dev
- qa
- staging
- prod
validations:
required: true
- type: input
id: branch
attributes:
label: Branch
description: Source branch (optional, defaults to main)
placeholder: "main"
validations:
required: false
- type: checkboxes
id: confirmations
attributes:
label: Pre-deployment Checklist
description: Please confirm
options:
- label: I have verified this version has passed all tests
required: true
- label: I have approval for production deployments (if applicable)
required: false2. Create the Workflow
Add this file to .github/workflows/custom-deploy.yml:
name: Custom Deploy IssueOps
on:
issues:
types: [opened, reopened]
jobs:
process_custom_deploy:
# Only run if issue contains our custom ID
if: contains(github.event.issue.body, '"id":"custom-deploy"')
runs-on: ubuntu-latest
steps:
- name: Parse Issue Body
id: parse
uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body;
const jsonMatch = body.match(/<!--\s*({[\s\S]*?})\s*-->/);
if (!jsonMatch) {
core.setFailed('No JSON payload found in issue');
return;
}
const payload = JSON.parse(jsonMatch[1]);
// Validate required fields
if (payload.id !== 'custom-deploy') {
core.setFailed('Not a custom-deploy request');
return;
}
if (!payload.version || !payload.environment) {
core.setFailed('Missing required fields: version and environment');
return;
}
// Set outputs for next steps
core.setOutput('version', payload.version);
core.setOutput('environment', payload.environment);
core.setOutput('branch', payload.branch || 'main');
- name: Post Processing Comment
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `🔄 Processing deployment request:
- Version: ${{ steps.parse.outputs.version }}
- Environment: ${{ steps.parse.outputs.environment }}
- Branch: ${{ steps.parse.outputs.branch }}
Please wait while I deploy...`
});
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ steps.parse.outputs.branch }}
- name: Authenticate to Salesforce
uses: flxbl-io/sfops-gh-actions/auth-hybrid@main
with:
environment: ${{ steps.parse.outputs.environment }}
- name: Deploy Version
id: deploy
run: |
# Your deployment logic here
echo "Deploying version ${{ steps.parse.outputs.version }}"
# Example: Deploy using sfp
sfp release \
--targetorg ${{ steps.parse.outputs.environment }} \
--releasedefn ${{ steps.parse.outputs.version }}
- name: Post Success Comment
if: success()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `✅ Deployment completed successfully!
**Deployment Details:**
- Version: ${{ steps.parse.outputs.version }}
- Environment: ${{ steps.parse.outputs.environment }}
- Status: Success
- Time: ${new Date().toISOString()}
You can now access the environment and verify the deployment.`
});
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
- name: Post Error Comment
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `❌ Deployment failed!
Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
You may need to:
- Verify the version exists
- Check environment permissions
- Review any validation errors`
});3. How Users Submit Requests
Users can submit requests in two ways:
Option A: Through DevCentral Forms Navigate to DevCentral's Service Catalogue and fill out the form.
Option B: Direct Issue Creation Create an issue directly in GitHub with JSON in the body:
Title: [DEPLOY] Production Release v1.2.3
Body:
...form fields displayed...
<!--
{
"id": "custom-deploy",
"version": "v1.2.3",
"environment": "prod",
"branch": "main"
}
-->The workflow detects this JSON and processes the request.
Key Concepts
Parsing JSON from Issues
const body = context.payload.issue.body;
const jsonMatch = body.match(/<!--\s*({[\s\S]*?})\s*-->/);
const payload = JSON.parse(jsonMatch[1]);Validating Requests
Always validate:
Required fields are present
Values are in expected format
User has necessary permissions
Providing Feedback
Use issue comments to:
Confirm request received
Show progress updates
Report success or failure
Provide next steps
Handling Errors
Post clear error messages
Include links to logs
Suggest fixes
Don't expose sensitive information
Extending Your Custom IssueOps
Add Approval Requirements
Check if certain users or teams approved:
- name: Check Approval
uses: actions/github-script@v7
with:
script: |
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const approved = comments.data.some(comment =>
comment.body.includes('/approve') &&
['approver1', 'approver2'].includes(comment.user.login)
);
if (!approved) {
core.setFailed('Deployment requires approval');
}Integrate with External Systems
Call webhooks or APIs:
- name: Notify External System
run: |
curl -X POST https://api.example.com/deployments \
-H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"environment": "${{ steps.parse.outputs.environment }}",
"version": "${{ steps.parse.outputs.version }}",
"status": "completed"
}'Chain with Built-in IssueOps
React to completed built-in IssueOps:
on:
issues:
types: [closed]
jobs:
post_processing:
if: contains(github.event.issue.labels.*.name, 'scratch-org')
runs-on: ubuntu-latest
steps:
# Extract org details from issue comments
# Run additional setupBest Practices
Keep it Simple: Start with basic functionality, add complexity as needed
Validate Everything: Check inputs before processing
Provide Clear Feedback: Users should know what's happening
Handle Errors Gracefully: Anticipate failures and provide helpful messages
Use Secrets: Never hardcode sensitive data
Test Thoroughly: Create test issues to verify workflow behavior
Testing Your Custom IssueOps
Create a test issue with your JSON payload
Watch the Actions tab for workflow execution
Verify comments are posted correctly
Confirm the operation completed as expected
Troubleshooting
Workflow not triggering:
Check the
ifcondition matches your JSONVerify workflow file is in correct location
Ensure workflow has necessary permissions
JSON parsing errors:
Validate JSON syntax
Check for special characters that need escaping
Ensure JSON is within HTML comment tags
Operations failing:
Review workflow logs in Actions tab
Check required secrets are configured
Verify permissions for resources being accessed
Next Steps
Customize the example for your needs
Add additional validation logic
Integrate with your existing CI/CD pipeline
Share your custom IssueOps with your team
Last updated