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 JSON - Verify 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