mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
Add complete automation SOP system for BlackRoad ERP
This implements the "Automate The Company" initiative with comprehensive
Standard Operating Procedures for GitHub + Salesforce + Asana integration.
New directory: sop/
├── workflows/ - End-to-end process documentation
│ ├── new-client-kickoff.md - Flagship workflow from deal → repos → Asana
│ └── release-pipeline.md - Deploy → update Salesforce + Asana
├── playbooks/ - Human-friendly checklists
│ └── brenda-new-client-checklist.md - Non-technical operator guide
├── salesforce/ - Salesforce automation specifications
│ ├── flows/opp-automation-onstagechange.md - Trigger on Closed Won
│ └── orchestrations/new-client-kickoff-orchestration.md - Multi-stage process
├── integrations/ - API integration specifications
│ ├── salesforce-to-github.md - Create repos from Salesforce
│ ├── github-to-salesforce.md - Update Salesforce after deploy
│ └── salesforce-to-asana.md - Create Asana projects from Salesforce
└── templates/ - Reusable templates
├── github-actions/ - CI/CD workflows (ci.yml, deploy.yml, safety.yml)
└── repo-template/ - Standard repo config (PR template, labels, branch protection)
Key Features:
- Event-driven automation (Closed Won → repos + Asana creation)
- GitHub Actions templates for CI/CD baseline
- Salesforce Flow & Orchestration specs
- Complete API integration documentation
- Operator-friendly playbooks
- Two-view approach (operator + engineer)
- No manual status syncing across systems
This provides the complete backbone for next-gen ERP automation.
This commit is contained in:
567
sop/integrations/github-to-salesforce.md
Normal file
567
sop/integrations/github-to-salesforce.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# Integration: GitHub → Salesforce
|
||||
|
||||
**Purpose:** Enable GitHub Actions to update Salesforce records after deployments
|
||||
**Direction:** GitHub calls Salesforce REST API
|
||||
**Authentication:** OAuth 2.0 (Connected App) or Named Credential
|
||||
**Status:** Active
|
||||
**Last Updated:** 2025-11-17
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This integration allows GitHub Actions workflows to:
|
||||
- Update Project records with deployment metadata
|
||||
- Create Deployment records for audit trail
|
||||
- Trigger Salesforce flows/automations
|
||||
- Close tasks or update statuses
|
||||
|
||||
**Key Use Case:** Auto-update Salesforce when code is deployed to production
|
||||
|
||||
---
|
||||
|
||||
## Authentication Setup
|
||||
|
||||
### Option A: Salesforce Connected App (Recommended)
|
||||
|
||||
**Benefits:**
|
||||
- OAuth 2.0 standard
|
||||
- Refresh tokens
|
||||
- IP restrictions
|
||||
- Better audit trail
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
#### 1. Create Connected App in Salesforce
|
||||
|
||||
1. **Setup → App Manager → New Connected App**
|
||||
|
||||
2. **Basic Information:**
|
||||
- Connected App Name: `GitHub Actions Integration`
|
||||
- API Name: `GitHub_Actions_Integration`
|
||||
- Contact Email: devops@blackroad.com
|
||||
|
||||
3. **API (Enable OAuth Settings):**
|
||||
- ✅ Enable OAuth Settings
|
||||
- Callback URL: `https://login.salesforce.com/services/oauth2/callback`
|
||||
- Selected OAuth Scopes:
|
||||
- `api` - Perform requests at any time
|
||||
- `refresh_token, offline_access` - Perform requests at any time
|
||||
|
||||
4. **Save** and wait 2-10 minutes for Consumer Key/Secret to be generated
|
||||
|
||||
5. **Get Credentials:**
|
||||
- Consumer Key (Client ID): `3MVG9...`
|
||||
- Consumer Secret (Client Secret): `ABC123...`
|
||||
|
||||
#### 2. Create GitHub Secrets
|
||||
|
||||
In each project repository (or organization-level):
|
||||
|
||||
1. Go to: Settings → Secrets and variables → Actions
|
||||
2. Add these secrets:
|
||||
|
||||
| Secret Name | Value | Description |
|
||||
|-------------|-------|-------------|
|
||||
| `SALESFORCE_INSTANCE_URL` | `https://your-domain.my.salesforce.com` | Salesforce instance URL |
|
||||
| `SALESFORCE_CLIENT_ID` | `3MVG9...` | Connected App Consumer Key |
|
||||
| `SALESFORCE_CLIENT_SECRET` | `ABC123...` | Connected App Consumer Secret |
|
||||
| `SALESFORCE_USERNAME` | `integration-user@blackroad.com` | Service account username |
|
||||
| `SALESFORCE_PASSWORD` | `password123` | Service account password |
|
||||
| `SALESFORCE_SECURITY_TOKEN` | `XYZ789...` | Security token for user |
|
||||
|
||||
**Security Best Practice:** Use a dedicated integration service account, not a personal user.
|
||||
|
||||
#### 3. Get Access Token in GitHub Actions
|
||||
|
||||
**Workflow Step:**
|
||||
|
||||
```yaml
|
||||
- name: Get Salesforce Access Token
|
||||
id: sf-auth
|
||||
run: |
|
||||
RESPONSE=$(curl -X POST "https://login.salesforce.com/services/oauth2/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=${{ secrets.SALESFORCE_CLIENT_ID }}" \
|
||||
-d "client_secret=${{ secrets.SALESFORCE_CLIENT_SECRET }}" \
|
||||
-d "username=${{ secrets.SALESFORCE_USERNAME }}" \
|
||||
-d "password=${{ secrets.SALESFORCE_PASSWORD }}${{ secrets.SALESFORCE_SECURITY_TOKEN }}")
|
||||
|
||||
ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
|
||||
INSTANCE_URL=$(echo $RESPONSE | jq -r '.instance_url')
|
||||
|
||||
echo "ACCESS_TOKEN=$ACCESS_TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "INSTANCE_URL=$INSTANCE_URL" >> $GITHUB_OUTPUT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option B: Salesforce REST API Endpoint (Webhook Style)
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Create Apex REST Endpoint:**
|
||||
|
||||
```apex
|
||||
@RestResource(urlMapping='/github/webhook')
|
||||
global class GitHubWebhookHandler {
|
||||
|
||||
@HttpPost
|
||||
global static String handleDeployment() {
|
||||
RestRequest req = RestContext.request;
|
||||
String body = req.requestBody.toString();
|
||||
|
||||
Map<String, Object> payload = (Map<String, Object>) JSON.deserializeUntyped(body);
|
||||
|
||||
String projectKey = (String) payload.get('project_key');
|
||||
String deployedAt = (String) payload.get('deployed_at');
|
||||
String gitSHA = (String) payload.get('git_sha');
|
||||
String deployedBy = (String) payload.get('deployed_by');
|
||||
|
||||
// Find Project by external ID
|
||||
Project__c project = [
|
||||
SELECT Id, Name
|
||||
FROM Project__c
|
||||
WHERE Project_Key__c = :projectKey
|
||||
LIMIT 1
|
||||
];
|
||||
|
||||
// Update Project
|
||||
project.Last_Deploy_At__c = Datetime.valueOf(deployedAt);
|
||||
project.Last_Deploy_SHA__c = gitSHA;
|
||||
project.Last_Deploy_Actor__c = deployedBy;
|
||||
project.Deploy_Status__c = 'Success';
|
||||
update project;
|
||||
|
||||
// Create Deployment record
|
||||
Deployment__c deployment = new Deployment__c(
|
||||
Project__c = project.Id,
|
||||
Deployed_At__c = Datetime.valueOf(deployedAt),
|
||||
Git_SHA__c = gitSHA,
|
||||
Deployed_By__c = deployedBy,
|
||||
Status__c = 'Success'
|
||||
);
|
||||
insert deployment;
|
||||
|
||||
return JSON.serialize(new Map<String, Object>{
|
||||
'success' => true,
|
||||
'project_id' => project.Id,
|
||||
'deployment_id' => deployment.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Configure Site Guest User Permissions:**
|
||||
- Grant access to `Project__c` and `Deployment__c` objects
|
||||
- Or use API key authentication in Apex
|
||||
|
||||
3. **Get Endpoint URL:**
|
||||
- `https://your-domain.my.salesforce.com/services/apexrest/github/webhook`
|
||||
|
||||
4. **Add to GitHub Secrets:**
|
||||
- `SALESFORCE_WEBHOOK_URL`: Full endpoint URL
|
||||
|
||||
---
|
||||
|
||||
## API Operations
|
||||
|
||||
### 1. Update Project Record (Upsert by External ID)
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
PATCH {INSTANCE_URL}/services/data/v58.0/sobjects/Project__c/Project_Key__c/{PROJECT_KEY}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {ACCESS_TOKEN}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"Last_Deploy_At__c": "2025-11-17T14:30:00Z",
|
||||
"Last_Deploy_SHA__c": "a1b2c3d4e5f6",
|
||||
"Last_Deploy_Branch__c": "main",
|
||||
"Last_Deploy_Actor__c": "github-user",
|
||||
"Deploy_Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Backend_URL__c": "https://backend.railway.app",
|
||||
"Release_Notes_URL__c": "https://github.com/org/repo/commit/a1b2c3d4e5f6"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK or 201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": "a0X5e000000XYZ1EAO",
|
||||
"success": true,
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
**GitHub Actions Implementation:**
|
||||
|
||||
```yaml
|
||||
- name: Update Salesforce Project record
|
||||
env:
|
||||
SF_INSTANCE_URL: ${{ steps.sf-auth.outputs.INSTANCE_URL }}
|
||||
SF_ACCESS_TOKEN: ${{ steps.sf-auth.outputs.ACCESS_TOKEN }}
|
||||
run: |
|
||||
# Extract PROJECT_KEY from repo name
|
||||
REPO_NAME="${{ github.repository }}"
|
||||
PROJECT_KEY=$(echo "$REPO_NAME" | sed -n 's/.*blackroad-\([A-Z0-9-]*\)-.*/\1/p')
|
||||
|
||||
curl -X PATCH \
|
||||
"$SF_INSTANCE_URL/services/data/v58.0/sobjects/Project__c/Project_Key__c/$PROJECT_KEY" \
|
||||
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Last_Deploy_At__c": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
|
||||
"Last_Deploy_SHA__c": "${{ github.sha }}",
|
||||
"Last_Deploy_Branch__c": "${{ github.ref_name }}",
|
||||
"Last_Deploy_Actor__c": "${{ github.actor }}",
|
||||
"Deploy_Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Release_Notes_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Create Deployment Record
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST {INSTANCE_URL}/services/data/v58.0/sobjects/Deployment__c
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {ACCESS_TOKEN}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"Name": "ACME-1042 - a1b2c3d4",
|
||||
"Project_Key__c": "ACME-1042",
|
||||
"Deployed_At__c": "2025-11-17T14:30:00Z",
|
||||
"Git_SHA__c": "a1b2c3d4e5f6",
|
||||
"Git_Branch__c": "main",
|
||||
"Deployed_By__c": "github-user",
|
||||
"Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Repository__c": "blackboxprogramming/blackroad-ACME-1042-backend",
|
||||
"Commit_URL__c": "https://github.com/blackboxprogramming/blackroad-ACME-1042-backend/commit/a1b2c3d4e5f6",
|
||||
"Duration_Seconds__c": 120
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": "a0Y5e000000ABC1EAO",
|
||||
"success": true,
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
**GitHub Actions Implementation:**
|
||||
|
||||
```yaml
|
||||
- name: Create Salesforce Deployment record
|
||||
run: |
|
||||
curl -X POST \
|
||||
"$SF_INSTANCE_URL/services/data/v58.0/sobjects/Deployment__c" \
|
||||
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Name": "'"$PROJECT_KEY"' - '"${GITHUB_SHA:0:8}"'",
|
||||
"Project_Key__c": "'"$PROJECT_KEY"'",
|
||||
"Deployed_At__c": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
|
||||
"Git_SHA__c": "${{ github.sha }}",
|
||||
"Git_Branch__c": "${{ github.ref_name }}",
|
||||
"Deployed_By__c": "${{ github.actor }}",
|
||||
"Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Repository__c": "${{ github.repository }}",
|
||||
"Commit_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Query Salesforce (Optional)
|
||||
|
||||
**Use Case:** Get Project metadata before deploying
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
GET {INSTANCE_URL}/services/data/v58.0/query?q=SELECT+Id,Name,Package_Type__c+FROM+Project__c+WHERE+Project_Key__c='ACME-1042'
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {ACCESS_TOKEN}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"totalSize": 1,
|
||||
"done": true,
|
||||
"records": [
|
||||
{
|
||||
"Id": "a0X5e000000XYZ1EAO",
|
||||
"Name": "Acme Corp - ACME-1042",
|
||||
"Package_Type__c": "OS"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required Salesforce Objects
|
||||
|
||||
### Deployment Custom Object
|
||||
|
||||
**API Name:** `Deployment__c`
|
||||
**Label:** Deployment
|
||||
**Record Name:** Deployment Name (Auto Number: `DEP-{0000}`)
|
||||
|
||||
#### Fields:
|
||||
|
||||
| Field API Name | Type | Length | Description |
|
||||
|----------------|------|--------|-------------|
|
||||
| `Project__c` | Lookup(Project__c) | N/A | Related project |
|
||||
| `Project_Key__c` | Text (External ID) | 20 | For upsert operations |
|
||||
| `Deployed_At__c` | DateTime | N/A | When deployment occurred |
|
||||
| `Git_SHA__c` | Text | 40 | Git commit SHA |
|
||||
| `Git_Branch__c` | Text | 100 | Git branch name |
|
||||
| `Deployed_By__c` | Text | 100 | GitHub username |
|
||||
| `Status__c` | Picklist | N/A | Success, Failed, Rollback, In Progress |
|
||||
| `Environment__c` | Picklist | N/A | Staging, Production |
|
||||
| `Repository__c` | Text | 255 | Full repo name (org/repo) |
|
||||
| `Commit_URL__c` | URL | 255 | Link to commit |
|
||||
| `Duration_Seconds__c` | Number(6,0) | N/A | Deploy duration |
|
||||
| `Error_Message__c` | Long Text Area | 32768 | If deploy failed |
|
||||
|
||||
---
|
||||
|
||||
## Complete GitHub Actions Workflow
|
||||
|
||||
**File:** `.github/workflows/notify-salesforce.yml`
|
||||
|
||||
```yaml
|
||||
name: Notify Salesforce After Deploy
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Deploy to Production"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
notify-salesforce:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Extract Project Key from repo name
|
||||
id: project-key
|
||||
run: |
|
||||
REPO_NAME="${{ github.repository }}"
|
||||
PROJECT_KEY=$(echo "$REPO_NAME" | sed -n 's/.*blackroad-\([A-Z0-9-]*\)-.*/\1/p')
|
||||
|
||||
if [ -z "$PROJECT_KEY" ]; then
|
||||
echo "Warning: Could not extract project key"
|
||||
PROJECT_KEY="UNKNOWN"
|
||||
fi
|
||||
|
||||
echo "PROJECT_KEY=$PROJECT_KEY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Authenticate with Salesforce
|
||||
id: sf-auth
|
||||
env:
|
||||
SF_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }}
|
||||
SF_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }}
|
||||
SF_USERNAME: ${{ secrets.SALESFORCE_USERNAME }}
|
||||
SF_PASSWORD: ${{ secrets.SALESFORCE_PASSWORD }}
|
||||
SF_SECURITY_TOKEN: ${{ secrets.SALESFORCE_SECURITY_TOKEN }}
|
||||
run: |
|
||||
RESPONSE=$(curl -X POST "https://login.salesforce.com/services/oauth2/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=$SF_CLIENT_ID" \
|
||||
-d "client_secret=$SF_CLIENT_SECRET" \
|
||||
-d "username=$SF_USERNAME" \
|
||||
-d "password=$SF_PASSWORD$SF_SECURITY_TOKEN")
|
||||
|
||||
ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
|
||||
INSTANCE_URL=$(echo $RESPONSE | jq -r '.instance_url')
|
||||
|
||||
echo "ACCESS_TOKEN=$ACCESS_TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "INSTANCE_URL=$INSTANCE_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update Salesforce Project record
|
||||
if: steps.project-key.outputs.PROJECT_KEY != 'UNKNOWN'
|
||||
env:
|
||||
SF_INSTANCE_URL: ${{ steps.sf-auth.outputs.INSTANCE_URL }}
|
||||
SF_ACCESS_TOKEN: ${{ steps.sf-auth.outputs.ACCESS_TOKEN }}
|
||||
PROJECT_KEY: ${{ steps.project-key.outputs.PROJECT_KEY }}
|
||||
run: |
|
||||
curl -X PATCH \
|
||||
"$SF_INSTANCE_URL/services/data/v58.0/sobjects/Project__c/Project_Key__c/$PROJECT_KEY" \
|
||||
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Last_Deploy_At__c": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
|
||||
"Last_Deploy_SHA__c": "${{ github.sha }}",
|
||||
"Last_Deploy_Branch__c": "${{ github.ref_name }}",
|
||||
"Last_Deploy_Actor__c": "${{ github.actor }}",
|
||||
"Deploy_Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Release_Notes_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||
}' || echo "Warning: Failed to update Project record"
|
||||
|
||||
- name: Create Deployment record
|
||||
if: steps.project-key.outputs.PROJECT_KEY != 'UNKNOWN'
|
||||
env:
|
||||
SF_INSTANCE_URL: ${{ steps.sf-auth.outputs.INSTANCE_URL }}
|
||||
SF_ACCESS_TOKEN: ${{ steps.sf-auth.outputs.ACCESS_TOKEN }}
|
||||
PROJECT_KEY: ${{ steps.project-key.outputs.PROJECT_KEY }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
"$SF_INSTANCE_URL/services/data/v58.0/sobjects/Deployment__c" \
|
||||
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Name": "'"$PROJECT_KEY"' - '"${GITHUB_SHA:0:8}"'",
|
||||
"Project_Key__c": "'"$PROJECT_KEY"'",
|
||||
"Deployed_At__c": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
|
||||
"Git_SHA__c": "${{ github.sha }}",
|
||||
"Git_Branch__c": "${{ github.ref_name }}",
|
||||
"Deployed_By__c": "${{ github.actor }}",
|
||||
"Status__c": "Success",
|
||||
"Environment__c": "Production",
|
||||
"Repository__c": "${{ github.repository }}",
|
||||
"Commit_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||
}' || echo "Warning: Failed to create Deployment record"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Status Code | Error | Cause | Solution |
|
||||
|-------------|-------|-------|----------|
|
||||
| 401 | Unauthorized | Token expired | Re-authenticate |
|
||||
| 403 | Forbidden | Insufficient permissions | Check user permissions in Salesforce |
|
||||
| 404 | Not Found | Project_Key__c doesn't exist | Verify project key extraction logic |
|
||||
| 400 | Bad Request | Invalid field value | Check datetime format (must be ISO 8601) |
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```yaml
|
||||
- name: Update Salesforce (with retries)
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
curl -X PATCH ... (Salesforce API call)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test (curl)
|
||||
|
||||
```bash
|
||||
# 1. Get access token
|
||||
ACCESS_TOKEN=$(curl -X POST "https://login.salesforce.com/services/oauth2/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=YOUR_CLIENT_ID" \
|
||||
-d "client_secret=YOUR_CLIENT_SECRET" \
|
||||
-d "username=YOUR_USERNAME" \
|
||||
-d "password=YOUR_PASSWORD_AND_TOKEN" | jq -r '.access_token')
|
||||
|
||||
# 2. Update Project
|
||||
curl -X PATCH \
|
||||
"https://your-domain.my.salesforce.com/services/data/v58.0/sobjects/Project__c/Project_Key__c/TEST-1234" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Last_Deploy_At__c": "2025-11-17T14:30:00Z",
|
||||
"Deploy_Status__c": "Success"
|
||||
}'
|
||||
```
|
||||
|
||||
### GitHub Actions Test
|
||||
|
||||
1. Push commit to test repo
|
||||
2. Trigger deploy workflow
|
||||
3. Verify Salesforce Project updated
|
||||
4. Check Deployment record created
|
||||
5. Review GitHub Actions logs for errors
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
**Track These Metrics:**
|
||||
|
||||
| Metric | Target | Alert Threshold |
|
||||
|--------|--------|-----------------|
|
||||
| Salesforce API Success Rate | > 98% | < 95% |
|
||||
| Avg API Response Time | < 1s | > 3s |
|
||||
| Failed Updates | < 2% | > 5% |
|
||||
|
||||
**Salesforce Report:** "Deployments by Status (Last 30 Days)"
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use Service Account:**
|
||||
- Create `integration-user@blackroad.com`
|
||||
- Assign minimal permissions
|
||||
- Enable API-only user (no login UI)
|
||||
|
||||
2. **Rotate Credentials:**
|
||||
- Rotate passwords every 90 days
|
||||
- Use GitHub encrypted secrets
|
||||
- Never commit credentials to git
|
||||
|
||||
3. **IP Restrictions:**
|
||||
- Whitelist GitHub Actions IP ranges in Salesforce
|
||||
- See: https://api.github.com/meta
|
||||
|
||||
4. **Audit Logging:**
|
||||
- Enable Salesforce Event Monitoring
|
||||
- Log all API calls
|
||||
- Review monthly
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Salesforce REST API Docs](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/)
|
||||
- [GitHub Actions: Deploy Workflow](../templates/github-actions/deploy.yml)
|
||||
- [Workflow: Release Pipeline](../workflows/release-pipeline.md)
|
||||
- [Integration: Salesforce → GitHub](./salesforce-to-github.md)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Version | Change | Author |
|
||||
|------|---------|--------|--------|
|
||||
| 2025-11-17 | 1.0 | Initial specification | Cece (Claude) |
|
||||
644
sop/integrations/salesforce-to-asana.md
Normal file
644
sop/integrations/salesforce-to-asana.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# Integration: Salesforce → Asana
|
||||
|
||||
**Purpose:** Enable Salesforce to create and manage Asana projects and tasks automatically
|
||||
**Direction:** Salesforce calls Asana REST API
|
||||
**Authentication:** Personal Access Token (PAT)
|
||||
**Status:** Active
|
||||
**Last Updated:** 2025-11-17
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This integration allows Salesforce Flows and Orchestrations to:
|
||||
- Create Asana projects
|
||||
- Add sections to projects
|
||||
- Create tasks with assignments and due dates
|
||||
- Update task status
|
||||
- Add comments to tasks
|
||||
|
||||
**Key Use Case:** Auto-create Asana project board when Opportunity closes
|
||||
|
||||
---
|
||||
|
||||
## Authentication Setup
|
||||
|
||||
### Asana Personal Access Token (PAT)
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Generate PAT:**
|
||||
- Go to: https://app.asana.com/0/my-apps
|
||||
- Click "Personal access tokens"
|
||||
- Click "+ New access token"
|
||||
- Name: `Salesforce Integration`
|
||||
- Copy token (starts with `1/...`)
|
||||
|
||||
2. **Store in Salesforce:**
|
||||
- Setup → Named Credentials → New
|
||||
- Name: `Asana_API`
|
||||
- URL: `https://app.asana.com/api/1.0`
|
||||
- Identity Type: Named Principal
|
||||
- Authentication Protocol: Custom
|
||||
- Header: `Authorization: Bearer {YOUR_PAT}`
|
||||
|
||||
**Alternative:** Use Custom Settings
|
||||
|
||||
```
|
||||
Setup → Custom Settings → New (Protected)
|
||||
Name: Asana_Settings__c
|
||||
Fields:
|
||||
- API_Token__c (Text, Encrypted)
|
||||
- Workspace_GID__c (Text)
|
||||
- Team_GID__c (Text)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required Asana Setup
|
||||
|
||||
### 1. Get Workspace GID
|
||||
|
||||
```bash
|
||||
curl "https://app.asana.com/api/1.0/workspaces" \
|
||||
-H "Authorization: Bearer YOUR_PAT"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"gid": "1234567890123456",
|
||||
"name": "BlackRoad Workspace",
|
||||
"resource_type": "workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Store:** `WORKSPACE_GID = "1234567890123456"`
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Team GID
|
||||
|
||||
```bash
|
||||
curl "https://app.asana.com/api/1.0/organizations/1234567890123456/teams" \
|
||||
-H "Authorization: Bearer YOUR_PAT"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"gid": "9876543210987654",
|
||||
"name": "Engineering",
|
||||
"resource_type": "team"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Store:** `TEAM_GID = "9876543210987654"`
|
||||
|
||||
---
|
||||
|
||||
### 3. Get User GIDs for Assignments
|
||||
|
||||
```bash
|
||||
curl "https://app.asana.com/api/1.0/users?workspace=1234567890123456" \
|
||||
-H "Authorization: Bearer YOUR_PAT"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"gid": "1111111111111111",
|
||||
"name": "Alice Developer",
|
||||
"email": "alice@blackroad.com"
|
||||
},
|
||||
{
|
||||
"gid": "2222222222222222",
|
||||
"name": "Bob DevOps",
|
||||
"email": "bob@blackroad.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Create Salesforce Mapping:**
|
||||
|
||||
| Role | Email | Asana GID |
|
||||
|------|-------|-----------|
|
||||
| DevOps Lead | bob@blackroad.com | 2222222222222222 |
|
||||
| Backend Lead | alice@blackroad.com | 1111111111111111 |
|
||||
| Customer Success | brenda@blackroad.com | 3333333333333333 |
|
||||
|
||||
Store in Custom Metadata Type: `Asana_User_Mapping__mdt`
|
||||
|
||||
---
|
||||
|
||||
## API Operations
|
||||
|
||||
### 1. Create Project
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://app.asana.com/api/1.0/projects
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {ASANA_PAT}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"workspace": "1234567890123456",
|
||||
"team": "9876543210987654",
|
||||
"name": "Acme Corp - ACME-1042",
|
||||
"notes": "Salesforce Project: https://your-domain.my.salesforce.com/a0X5e000000XYZ1EAO\n\nRepos:\n- Backend: https://github.com/blackboxprogramming/blackroad-ACME-1042-backend\n- Frontend: https://github.com/blackboxprogramming/blackroad-ACME-1042-frontend\n- Ops: https://github.com/blackboxprogramming/blackroad-ACME-1042-ops",
|
||||
"color": "light-green",
|
||||
"default_view": "board",
|
||||
"public": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gid": "5555555555555555",
|
||||
"name": "Acme Corp - ACME-1042",
|
||||
"permalink_url": "https://app.asana.com/0/5555555555555555/list"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Flow Implementation:**
|
||||
|
||||
```yaml
|
||||
Element: HTTP Callout
|
||||
Method: POST
|
||||
Endpoint: {!$Credential.Asana_API}/projects
|
||||
Headers:
|
||||
- Authorization: Bearer {!$Credential.Asana_API.Token}
|
||||
- Content-Type: application/json
|
||||
Body:
|
||||
{
|
||||
"data": {
|
||||
"workspace": "{!$CustomMetadata.Asana_Settings__mdt.Workspace_GID__c}",
|
||||
"team": "{!$CustomMetadata.Asana_Settings__mdt.Team_GID__c}",
|
||||
"name": "{!varProject.Name}",
|
||||
"notes": "Salesforce: {!varProject.Id}\nRepos:\n- {!varProject.Backend_Repo_URL__c}",
|
||||
"color": "light-green",
|
||||
"default_view": "board"
|
||||
}
|
||||
}
|
||||
|
||||
Store Response: varAsanaResponse
|
||||
Parse:
|
||||
- varAsanaProjectGID = {!varAsanaResponse.data.gid}
|
||||
- varAsanaProjectURL = {!varAsanaResponse.data.permalink_url}
|
||||
|
||||
Update Project Record:
|
||||
- Asana_Project_URL__c = {!varAsanaProjectURL}
|
||||
- Asana_Project_GID__c = {!varAsanaProjectGID}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Create Section
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://app.asana.com/api/1.0/projects/{PROJECT_GID}/sections
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"name": "Discovery"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gid": "6666666666666666",
|
||||
"name": "Discovery"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Implementation:**
|
||||
|
||||
```yaml
|
||||
Element: Loop
|
||||
Collection: ["Discovery", "Architecture", "Build", "Testing", "Go-Live"]
|
||||
Current Item: varSectionName
|
||||
|
||||
Inside Loop:
|
||||
- HTTP Callout
|
||||
- Endpoint: {!$Credential.Asana_API}/projects/{!varAsanaProjectGID}/sections
|
||||
- Body: {"data": {"name": "{!varSectionName}"}}
|
||||
- Store Response: varSectionResponse
|
||||
- Add to Collection: varSectionGIDs[{!varSectionName}] = {!varSectionResponse.data.gid}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Create Task
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://app.asana.com/api/1.0/tasks
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"projects": ["5555555555555555"],
|
||||
"name": "Confirm domain + DNS with client",
|
||||
"notes": "Get final domain, subdomain, and DNS setup requirements from client.\n\nSalesforce Project: https://your-domain.my.salesforce.com/a0X5e000000XYZ1EAO",
|
||||
"assignee": "3333333333333333",
|
||||
"due_on": "2025-11-20",
|
||||
"memberships": [
|
||||
{
|
||||
"project": "5555555555555555",
|
||||
"section": "6666666666666666"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gid": "7777777777777777",
|
||||
"name": "Confirm domain + DNS with client",
|
||||
"permalink_url": "https://app.asana.com/0/5555555555555555/7777777777777777"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Implementation:**
|
||||
|
||||
```yaml
|
||||
Element: Loop
|
||||
Collection: varTaskDefinitions (custom metadata or JSON)
|
||||
Current Item: varTask
|
||||
|
||||
Inside Loop:
|
||||
- Calculate Due Date
|
||||
Formula: TODAY() + {!varTask.DaysOffset}
|
||||
|
||||
- Lookup Assignee GID
|
||||
From: Asana_User_Mapping__mdt
|
||||
Match: Role = {!varTask.AssigneeRole}
|
||||
|
||||
- HTTP Callout
|
||||
- Endpoint: {!$Credential.Asana_API}/tasks
|
||||
- Body:
|
||||
{
|
||||
"data": {
|
||||
"projects": ["{!varAsanaProjectGID}"],
|
||||
"name": "{!varTask.Name}",
|
||||
"notes": "{!varTask.Description}\n\nSalesforce: {!varProject.Id}",
|
||||
"assignee": "{!varAssigneeGID}",
|
||||
"due_on": "{!varDueDate}",
|
||||
"memberships": [{
|
||||
"project": "{!varAsanaProjectGID}",
|
||||
"section": "{!varSectionGIDs[varTask.Section]}"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Update Task (Mark Complete)
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
PUT https://app.asana.com/api/1.0/tasks/{TASK_GID}
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"completed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gid": "7777777777777777",
|
||||
"completed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Add Comment to Task
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://app.asana.com/api/1.0/tasks/{TASK_GID}/stories
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"text": "✅ Deployed v0.1.3 to production\n\n**Commit:** a1b2c3d4\n**By:** github-user\n**Time:** 2025-11-17 14:30 UTC\n**Link:** https://github.com/org/repo/commit/a1b2c3d4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gid": "8888888888888888",
|
||||
"text": "✅ Deployed v0.1.3..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task Template Definition
|
||||
|
||||
**Store in Salesforce Custom Metadata:** `Asana_Task_Template__mdt`
|
||||
|
||||
| Label | API Name | Section__c | Days_Offset__c | Assignee_Role__c |
|
||||
|-------|----------|------------|----------------|------------------|
|
||||
| Confirm domain + DNS | Confirm_Domain_DNS | Discovery | 1 | Sales Ops |
|
||||
| Gather branding assets | Gather_Branding | Discovery | 1 | Design |
|
||||
| Wire up Railway/Cloudflare | Wire_Up_Envs | Architecture | 3 | DevOps |
|
||||
| Enable CI/CD secrets | Enable_Secrets | Architecture | 3 | DevOps |
|
||||
| Set up database | Setup_Database | Build | 5 | Backend |
|
||||
| Implement authentication | Implement_Auth | Build | 7 | Backend |
|
||||
| Run E2E tests | Run_E2E_Tests | Testing | 12 | QA |
|
||||
| Final client walkthrough | Final_Walkthrough | Go-Live | 18 | Customer Success |
|
||||
| Deploy to production | Deploy_Production | Go-Live | 19 | DevOps |
|
||||
|
||||
**Query in Flow:**
|
||||
|
||||
```apex
|
||||
[SELECT Label, Section__c, Days_Offset__c, Assignee_Role__c, Description__c
|
||||
FROM Asana_Task_Template__mdt
|
||||
ORDER BY Days_Offset__c ASC]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Salesforce Flow Example
|
||||
|
||||
**Flow Name:** `Asana_Project_Setup`
|
||||
|
||||
**Input Variables:**
|
||||
- `ProjectRecordId` (Text)
|
||||
- `AsanaProjectGID` (Text) - from Create Project step
|
||||
|
||||
**Steps:**
|
||||
|
||||
```yaml
|
||||
1. Get Project Record
|
||||
- Object: Project__c
|
||||
- Filter: Id = {!ProjectRecordId}
|
||||
|
||||
2. Create Asana Project
|
||||
- HTTP Callout (as documented)
|
||||
- Store: varAsanaProjectGID
|
||||
|
||||
3. Create Sections
|
||||
- Loop: ["Discovery", "Architecture", "Build", "Testing", "Go-Live"]
|
||||
- HTTP Callout per section
|
||||
- Store GIDs in Map: varSectionGIDs
|
||||
|
||||
4. Get Task Templates
|
||||
- Get Records: Asana_Task_Template__mdt
|
||||
- Store: varTaskTemplates
|
||||
|
||||
5. Loop: Create Tasks
|
||||
- For Each: varTaskTemplates
|
||||
- Calculate due date: TODAY() + DaysOffset
|
||||
- Lookup assignee GID from Asana_User_Mapping__mdt
|
||||
- HTTP Callout to create task
|
||||
- Link task to correct section
|
||||
|
||||
6. Update Project Record
|
||||
- Asana_Project_URL__c = {!varAsanaResponse.data.permalink_url}
|
||||
- Asana_Project_GID__c = {!varAsanaProjectGID}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Mapping Setup
|
||||
|
||||
**Custom Metadata Type:** `Asana_User_Mapping__mdt`
|
||||
|
||||
**Fields:**
|
||||
- `Role__c` (Text) - "DevOps", "Backend", "Customer Success", etc.
|
||||
- `Email__c` (Email)
|
||||
- `Asana_GID__c` (Text)
|
||||
|
||||
**Records:**
|
||||
|
||||
| Label | Role__c | Email__c | Asana_GID__c |
|
||||
|-------|---------|----------|--------------|
|
||||
| DevOps Team | DevOps | devops@blackroad.com | 2222222222222222 |
|
||||
| Backend Team | Backend | backend@blackroad.com | 1111111111111111 |
|
||||
| Customer Success | Customer Success | brenda@blackroad.com | 3333333333333333 |
|
||||
| QA Team | QA | qa@blackroad.com | 4444444444444444 |
|
||||
|
||||
**Query in Flow:**
|
||||
|
||||
```apex
|
||||
[SELECT Asana_GID__c
|
||||
FROM Asana_User_Mapping__mdt
|
||||
WHERE Role__c = :assigneeRole
|
||||
LIMIT 1]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Status Code | Error | Cause | Solution |
|
||||
|-------------|-------|-------|----------|
|
||||
| 400 | Bad Request | Invalid GID or payload | Verify workspace/team GIDs |
|
||||
| 401 | Unauthorized | Invalid token | Regenerate PAT in Asana |
|
||||
| 403 | Forbidden | No access to workspace | Check PAT has workspace access |
|
||||
| 404 | Not Found | Project/task doesn't exist | Verify GID is correct |
|
||||
| 429 | Too Many Requests | Rate limit exceeded | Implement exponential backoff |
|
||||
|
||||
### Salesforce Fault Path
|
||||
|
||||
```yaml
|
||||
Fault Path:
|
||||
- Element: Create Case
|
||||
Subject: "Asana Integration Error: {!$Flow.FaultMessage}"
|
||||
Description: "Failed to create Asana project for: {!varProject.Name}\n\nError: {!$Flow.FaultMessage}"
|
||||
Priority: Medium
|
||||
Type: "Automation Bug"
|
||||
|
||||
- Element: Send Email
|
||||
To: ops@blackroad.com
|
||||
Subject: "Asana Automation Failed - Manual Project Needed"
|
||||
Body: "Project: {!varProject.Name}\nKey: {!varProject.Project_Key__c}\n\nPlease create Asana project manually."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
**Asana API Rate Limits:**
|
||||
- 1,500 requests per minute per user
|
||||
- Burst: Up to 60 requests in the first second
|
||||
|
||||
**Per Asana Project Creation:**
|
||||
- 1 request: Create project
|
||||
- 5 requests: Create sections
|
||||
- 8-10 requests: Create tasks
|
||||
- **Total:** ~15 requests
|
||||
|
||||
**Best Practices:**
|
||||
- Can create ~100 projects per minute
|
||||
- Add delay between operations if hitting limits
|
||||
- Use exponential backoff on 429 errors
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test (curl)
|
||||
|
||||
```bash
|
||||
# 1. Create project
|
||||
curl -X POST "https://app.asana.com/api/1.0/projects" \
|
||||
-H "Authorization: Bearer YOUR_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"data": {
|
||||
"workspace": "1234567890123456",
|
||||
"name": "Test Project - DELETE ME"
|
||||
}
|
||||
}'
|
||||
|
||||
# Response: Get project GID (e.g., 5555555555555555)
|
||||
|
||||
# 2. Create section
|
||||
curl -X POST "https://app.asana.com/api/1.0/projects/5555555555555555/sections" \
|
||||
-H "Authorization: Bearer YOUR_PAT" \
|
||||
-d '{"data": {"name": "To Do"}}'
|
||||
|
||||
# 3. Create task
|
||||
curl -X POST "https://app.asana.com/api/1.0/tasks" \
|
||||
-H "Authorization: Bearer YOUR_PAT" \
|
||||
-d '{
|
||||
"data": {
|
||||
"projects": ["5555555555555555"],
|
||||
"name": "Test task"
|
||||
}
|
||||
}'
|
||||
|
||||
# 4. Clean up - delete project
|
||||
curl -X DELETE "https://app.asana.com/api/1.0/projects/5555555555555555" \
|
||||
-H "Authorization: Bearer YOUR_PAT"
|
||||
```
|
||||
|
||||
### Salesforce Sandbox Test
|
||||
|
||||
1. Create test Project record
|
||||
2. Run flow: `Asana_Project_Setup`
|
||||
3. Verify:
|
||||
- Project created in Asana
|
||||
- Sections present
|
||||
- Tasks created with correct assignees
|
||||
- Due dates calculated correctly
|
||||
4. Clean up test project in Asana
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
**Track These Metrics:**
|
||||
|
||||
| Metric | Target | Alert Threshold |
|
||||
|--------|--------|-----------------|
|
||||
| Asana API Success Rate | > 98% | < 95% |
|
||||
| Avg Project Creation Time | < 30s | > 60s |
|
||||
| Failed Project Creations | < 2% | > 5% |
|
||||
| Tasks Created Per Project | ~8-10 | < 5 or > 15 |
|
||||
|
||||
**Salesforce Custom Object:** `Asana_API_Log__c`
|
||||
|
||||
**Fields:**
|
||||
- Operation__c (Create Project, Create Task, etc.)
|
||||
- Project__c (Lookup)
|
||||
- Status__c (Success, Failed)
|
||||
- Response_Time__c
|
||||
- Error_Message__c
|
||||
- Timestamp__c
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **PAT Security:**
|
||||
- Never share PAT
|
||||
- Use encrypted custom settings
|
||||
- Rotate every 90 days
|
||||
- Generate separate PAT per integration (if multiple)
|
||||
|
||||
2. **Project Visibility:**
|
||||
- Set `public: false` for client projects
|
||||
- Only share with relevant team members
|
||||
- Review permissions quarterly
|
||||
|
||||
3. **Audit Logging:**
|
||||
- Log all Asana API calls in Salesforce
|
||||
- Review monthly
|
||||
- Track who accessed projects
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Asana API Docs: Projects](https://developers.asana.com/docs/projects)
|
||||
- [Asana API Docs: Tasks](https://developers.asana.com/docs/tasks)
|
||||
- [Salesforce Orchestration: New Client Kickoff](../salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||
- [Workflow: New Client Kickoff](../workflows/new-client-kickoff.md)
|
||||
- [Integration: Salesforce → GitHub](./salesforce-to-github.md)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Version | Change | Author |
|
||||
|------|---------|--------|--------|
|
||||
| 2025-11-17 | 1.0 | Initial specification | Cece (Claude) |
|
||||
539
sop/integrations/salesforce-to-github.md
Normal file
539
sop/integrations/salesforce-to-github.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# Integration: Salesforce → GitHub
|
||||
|
||||
**Purpose:** Enable Salesforce to create and configure GitHub repositories automatically
|
||||
**Direction:** Salesforce calls GitHub REST API
|
||||
**Authentication:** GitHub App or Personal Access Token
|
||||
**Status:** Active
|
||||
**Last Updated:** 2025-11-17
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This integration allows Salesforce Flows and Orchestrations to:
|
||||
- Create new repositories
|
||||
- Configure repository settings
|
||||
- Add labels, branch protection, workflows
|
||||
- Manage repository secrets
|
||||
|
||||
**Key Use Case:** Automatic repo creation when Opportunity moves to "Closed Won"
|
||||
|
||||
---
|
||||
|
||||
## Authentication Setup
|
||||
|
||||
### Option A: GitHub App (Recommended for Production)
|
||||
|
||||
**Benefits:**
|
||||
- More secure (short-lived tokens)
|
||||
- Better rate limits
|
||||
- Granular permissions
|
||||
- Audit trail
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Create GitHub App:**
|
||||
- Go to: https://github.com/organizations/blackboxprogramming/settings/apps
|
||||
- Click "New GitHub App"
|
||||
- Name: `BlackRoad Salesforce Integration`
|
||||
- Homepage URL: `https://blackroad.app`
|
||||
- Webhook URL: (leave blank for now, covered in github-to-salesforce.md)
|
||||
|
||||
2. **Permissions:**
|
||||
- Repository permissions:
|
||||
- Administration: Read & Write
|
||||
- Contents: Read & Write
|
||||
- Metadata: Read-only
|
||||
- Secrets: Read & Write
|
||||
- Workflows: Read & Write
|
||||
- Organization permissions:
|
||||
- Members: Read-only
|
||||
|
||||
3. **Install App:**
|
||||
- Install app on organization: `blackboxprogramming`
|
||||
- Select: All repositories (or specific repos)
|
||||
|
||||
4. **Generate Private Key:**
|
||||
- Download private key (`.pem` file)
|
||||
- Store securely in Salesforce
|
||||
|
||||
5. **Get App Details:**
|
||||
- App ID: `123456`
|
||||
- Installation ID: `789012`
|
||||
|
||||
**Salesforce Named Credential:**
|
||||
- Name: `GitHub_API`
|
||||
- URL: `https://api.github.com`
|
||||
- Identity Type: Named Principal
|
||||
- Authentication Protocol: Custom
|
||||
- Custom Authentication: Use Apex class to generate JWT → exchange for installation access token
|
||||
|
||||
**Apex Class for Token Generation:**
|
||||
|
||||
```apex
|
||||
public class GitHubAppTokenProvider {
|
||||
private static final String GITHUB_APP_ID = '123456';
|
||||
private static final String GITHUB_APP_PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE'; // Store in Protected Custom Setting
|
||||
|
||||
public static String getInstallationToken() {
|
||||
// 1. Generate JWT
|
||||
String jwt = generateJWT();
|
||||
|
||||
// 2. Exchange JWT for installation access token
|
||||
HttpRequest req = new HttpRequest();
|
||||
req.setEndpoint('https://api.github.com/app/installations/789012/access_tokens');
|
||||
req.setMethod('POST');
|
||||
req.setHeader('Authorization', 'Bearer ' + jwt);
|
||||
req.setHeader('Accept', 'application/vnd.github+json');
|
||||
|
||||
Http http = new Http();
|
||||
HttpResponse res = http.send(req);
|
||||
|
||||
if (res.getStatusCode() == 201) {
|
||||
Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
|
||||
return (String) result.get('token');
|
||||
}
|
||||
|
||||
throw new CalloutException('Failed to get GitHub access token: ' + res.getBody());
|
||||
}
|
||||
|
||||
private static String generateJWT() {
|
||||
// Use JWT library or implement JWT generation
|
||||
// See: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option B: Personal Access Token (Quick Start / Testing)
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Create PAT:**
|
||||
- Go to: https://github.com/settings/tokens
|
||||
- Click "Generate new token (classic)"
|
||||
- Scopes:
|
||||
- `repo` (full control)
|
||||
- `workflow` (update workflows)
|
||||
- `admin:org` (read org)
|
||||
|
||||
2. **Store in Salesforce:**
|
||||
- Setup → Named Credentials → New
|
||||
- Name: `GitHub_API`
|
||||
- URL: `https://api.github.com`
|
||||
- Identity Type: Named Principal
|
||||
- Authentication Protocol: Password Authentication
|
||||
- Username: (your GitHub username)
|
||||
- Password: (paste PAT)
|
||||
|
||||
**⚠️ Security Note:** PAT never expires unless you set an expiration. For production, use GitHub App.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints & Payloads
|
||||
|
||||
### 1. Create Repository
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://api.github.com/orgs/blackboxprogramming/repos
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {GITHUB_TOKEN}
|
||||
Accept: application/vnd.github+json
|
||||
X-GitHub-Api-Version: 2022-11-28
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"name": "blackroad-ACME-1042-backend",
|
||||
"description": "Backend for Acme Corp (ACME-1042)",
|
||||
"private": true,
|
||||
"auto_init": true,
|
||||
"gitignore_template": "Python",
|
||||
"license_template": "mit"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": 123456789,
|
||||
"name": "blackroad-ACME-1042-backend",
|
||||
"full_name": "blackboxprogramming/blackroad-ACME-1042-backend",
|
||||
"html_url": "https://github.com/blackboxprogramming/blackroad-ACME-1042-backend",
|
||||
"clone_url": "https://github.com/blackboxprogramming/blackroad-ACME-1042-backend.git",
|
||||
"default_branch": "main",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Flow Implementation:**
|
||||
|
||||
```yaml
|
||||
Element: HTTP Callout
|
||||
Method: POST
|
||||
Endpoint: {!$Credential.GitHub_API}/orgs/blackboxprogramming/repos
|
||||
Headers:
|
||||
- Authorization: Bearer {!$Credential.GitHub_API.AccessToken}
|
||||
- Accept: application/vnd.github+json
|
||||
- Content-Type: application/json
|
||||
Body: (JSON from above, with merge fields)
|
||||
|
||||
Store Response In: varRepoResponse
|
||||
Parse:
|
||||
- varRepoURL = {!varRepoResponse.html_url}
|
||||
- varRepoName = {!varRepoResponse.name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Create Labels
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://api.github.com/repos/blackboxprogramming/{REPO_NAME}/labels
|
||||
```
|
||||
|
||||
**Payload (repeat for each label):**
|
||||
```json
|
||||
{
|
||||
"name": "type:feature",
|
||||
"color": "0E8A16",
|
||||
"description": "New feature or enhancement"
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Implementation:**
|
||||
|
||||
Use a loop to create multiple labels from a JSON dataset:
|
||||
|
||||
```yaml
|
||||
Element: Loop
|
||||
Collection: Parse JSON from sop/templates/repo-template/.github/labels.json
|
||||
Current Item: varLabel
|
||||
|
||||
Inside Loop:
|
||||
- HTTP Callout
|
||||
- Endpoint: {!$Credential.GitHub_API}/repos/blackboxprogramming/{!varRepoName}/labels
|
||||
- Body: {!varLabel}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Apply Branch Protection
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
PUT https://api.github.com/repos/blackboxprogramming/{REPO_NAME}/branches/main/protection
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"contexts": ["test (3.11)", "test (3.12)", "lint", "build"]
|
||||
},
|
||||
"enforce_admins": true,
|
||||
"required_pull_request_reviews": {
|
||||
"dismiss_stale_reviews": true,
|
||||
"require_code_owner_reviews": true,
|
||||
"required_approving_review_count": 1
|
||||
},
|
||||
"restrictions": null,
|
||||
"required_linear_history": true,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"url": "https://api.github.com/repos/blackboxprogramming/blackroad-ACME-1042-backend/branches/main/protection",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Create Workflow Files
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
PUT https://api.github.com/repos/blackboxprogramming/{REPO_NAME}/contents/.github/workflows/ci.yml
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"message": "Add CI workflow",
|
||||
"content": "{BASE64_ENCODED_CI_YML}",
|
||||
"branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
**Salesforce Implementation:**
|
||||
|
||||
1. Store workflow YAML templates in Salesforce Static Resources:
|
||||
- `ci_yml`
|
||||
- `deploy_yml`
|
||||
- `safety_yml`
|
||||
|
||||
2. For each workflow:
|
||||
- Load static resource
|
||||
- Base64 encode content (use Apex)
|
||||
- PUT to GitHub
|
||||
|
||||
**Apex Helper:**
|
||||
```apex
|
||||
public static String base64EncodeStaticResource(String resourceName) {
|
||||
StaticResource sr = [SELECT Body FROM StaticResource WHERE Name = :resourceName LIMIT 1];
|
||||
return EncodingUtil.base64Encode(sr.Body);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Add Repository Secrets
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
PUT https://api.github.com/repos/blackboxprogramming/{REPO_NAME}/actions/secrets/{SECRET_NAME}
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"encrypted_value": "{ENCRYPTED_SECRET}",
|
||||
"key_id": "{PUBLIC_KEY_ID}"
|
||||
}
|
||||
```
|
||||
|
||||
**Pre-requisite:** Get repository public key
|
||||
|
||||
```
|
||||
GET https://api.github.com/repos/blackboxprogramming/{REPO_NAME}/actions/secrets/public-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"key_id": "012345678912345678",
|
||||
"key": "BASE64_PUBLIC_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
**Encryption:**
|
||||
|
||||
Use libsodium sealed boxes (NaCl) to encrypt secrets.
|
||||
|
||||
**Apex Implementation:** Use external service or pre-encrypted values.
|
||||
|
||||
**Secrets to Add:**
|
||||
|
||||
| Secret Name | Value Source |
|
||||
|-------------|--------------|
|
||||
| `PROJECT_KEY` | From Project__c.Project_Key__c |
|
||||
| `SALESFORCE_INSTANCE_URL` | From Salesforce |
|
||||
| `SALESFORCE_ACCESS_TOKEN` | Generate Connected App token |
|
||||
| `RAILWAY_TOKEN` | From Salesforce Custom Setting |
|
||||
| `CLOUDFLARE_API_TOKEN` | From Salesforce Custom Setting |
|
||||
| `ASANA_PAT` | From Salesforce Custom Setting |
|
||||
|
||||
---
|
||||
|
||||
## Complete Flow Example
|
||||
|
||||
**Salesforce Flow: Create GitHub Repos for Project**
|
||||
|
||||
```yaml
|
||||
Flow Name: GitHub_Repo_Setup
|
||||
|
||||
Input Variables:
|
||||
- ProjectRecordId (Text)
|
||||
|
||||
Steps:
|
||||
|
||||
1. Get Project Record
|
||||
- Object: Project__c
|
||||
- Filter: Id = {!ProjectRecordId}
|
||||
- Store: varProject
|
||||
|
||||
2. Loop: For Each Repo Type
|
||||
- Collection: ["backend", "frontend", "ops"]
|
||||
- Current Item: varRepoType
|
||||
|
||||
2.1: Create Repo
|
||||
- HTTP Callout (as documented above)
|
||||
- Store Response: varRepoResponse
|
||||
|
||||
2.2: Create Labels
|
||||
- Loop through labels.json
|
||||
- HTTP Callout for each label
|
||||
|
||||
2.3: Apply Branch Protection
|
||||
- HTTP Callout (as documented)
|
||||
|
||||
2.4: Create Workflow Files
|
||||
- For each: ci.yml, deploy.yml, safety.yml
|
||||
- HTTP Callout to create file
|
||||
|
||||
2.5: Add Secrets
|
||||
- Get public key
|
||||
- Encrypt secrets
|
||||
- PUT each secret
|
||||
|
||||
2.6: Update Project Record
|
||||
- Assignment:
|
||||
- Backend_Repo_URL__c = {!varRepoResponse.html_url} (if backend)
|
||||
- Frontend_Repo_URL__c = {!varRepoResponse.html_url} (if frontend)
|
||||
- Ops_Repo_URL__c = {!varRepoResponse.html_url} (if ops)
|
||||
|
||||
3. Update Project Record with all URLs
|
||||
- Update Record: Project__c
|
||||
|
||||
4. Send Success Notification
|
||||
- Post to Chatter or send email
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Status Code | Error | Cause | Solution |
|
||||
|-------------|-------|-------|----------|
|
||||
| 401 | Unauthorized | Invalid token | Refresh GitHub App token or regenerate PAT |
|
||||
| 403 | Forbidden | Insufficient permissions | Check GitHub App/PAT scopes |
|
||||
| 422 | Unprocessable Entity | Repo name already exists | Check for existing repo first |
|
||||
| 422 | Validation Failed | Branch protection: required check doesn't exist | Create workflow first, then apply protection |
|
||||
| 404 | Not Found | Repo or resource doesn't exist | Verify repo was created successfully |
|
||||
|
||||
### Salesforce Fault Path
|
||||
|
||||
```yaml
|
||||
Fault Path:
|
||||
- Element: Create Case
|
||||
Subject: "GitHub Integration Error: {!$Flow.FaultMessage}"
|
||||
Description: "Failed to create GitHub repo for Project: {!varProject.Name}\n\nError: {!$Flow.FaultMessage}"
|
||||
Priority: High
|
||||
OwnerId: {!DevOpsQueueId}
|
||||
|
||||
- Element: Send Email
|
||||
To: devops@blackroad.com
|
||||
Subject: "GitHub Automation Failed"
|
||||
Body: (include error details)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test (Postman / curl)
|
||||
|
||||
```bash
|
||||
# 1. Create repo
|
||||
curl -X POST \
|
||||
https://api.github.com/orgs/blackboxprogramming/repos \
|
||||
-H "Authorization: token YOUR_PAT" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-d '{
|
||||
"name": "test-repo-delete-me",
|
||||
"private": true,
|
||||
"auto_init": true
|
||||
}'
|
||||
|
||||
# 2. Create label
|
||||
curl -X POST \
|
||||
https://api.github.com/repos/blackboxprogramming/test-repo-delete-me/labels \
|
||||
-H "Authorization: token YOUR_PAT" \
|
||||
-d '{
|
||||
"name": "type:test",
|
||||
"color": "BADA55"
|
||||
}'
|
||||
|
||||
# 3. Clean up
|
||||
curl -X DELETE \
|
||||
https://api.github.com/repos/blackboxprogramming/test-repo-delete-me \
|
||||
-H "Authorization: token YOUR_PAT"
|
||||
```
|
||||
|
||||
### Salesforce Sandbox Test
|
||||
|
||||
1. Create test Project record
|
||||
2. Run flow: `GitHub_Repo_Setup` with test Project ID
|
||||
3. Verify:
|
||||
- Repos created in GitHub
|
||||
- Labels applied
|
||||
- Branch protection enabled
|
||||
- Workflows present
|
||||
- Secrets added
|
||||
4. Clean up test repos
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
**GitHub API Rate Limits:**
|
||||
- PAT: 5,000 requests/hour
|
||||
- GitHub App: 15,000 requests/hour
|
||||
|
||||
**Per Repository Creation:**
|
||||
- Approximately 50-100 API calls (1 repo + labels + protection + workflows + secrets)
|
||||
- Can create ~50-300 repos per hour
|
||||
|
||||
**Best Practices:**
|
||||
- Use GitHub App for better limits
|
||||
- Implement exponential backoff on 403 (rate limit exceeded)
|
||||
- Cache public keys for secret encryption
|
||||
- Batch operations where possible
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
**Track These Metrics:**
|
||||
|
||||
| Metric | Target | Alert Threshold |
|
||||
|--------|--------|-----------------|
|
||||
| API Success Rate | > 98% | < 95% |
|
||||
| Avg Response Time | < 2s | > 5s |
|
||||
| Rate Limit Usage | < 50% | > 80% |
|
||||
| Failed Repo Creations | < 2% | > 5% |
|
||||
|
||||
**Salesforce Custom Object:** `GitHub_API_Log__c`
|
||||
|
||||
**Fields:**
|
||||
- Operation__c (Create Repo, Add Label, etc.)
|
||||
- Project__c (Lookup)
|
||||
- Status__c (Success, Failed)
|
||||
- Status_Code__c (200, 201, 422, etc.)
|
||||
- Error_Message__c
|
||||
- Response_Time__c
|
||||
- Timestamp__c
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [GitHub API Docs: Repositories](https://docs.github.com/en/rest/repos/repos)
|
||||
- [GitHub API Docs: Branch Protection](https://docs.github.com/en/rest/branches/branch-protection)
|
||||
- [GitHub API Docs: Secrets](https://docs.github.com/en/rest/actions/secrets)
|
||||
- [Salesforce Orchestration: New Client Kickoff](../salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||
- [Workflow: New Client Kickoff](../workflows/new-client-kickoff.md)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Version | Change | Author |
|
||||
|------|---------|--------|--------|
|
||||
| 2025-11-17 | 1.0 | Initial specification | Cece (Claude) |
|
||||
Reference in New Issue
Block a user