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:
580
sop/README.md
Normal file
580
sop/README.md
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
# BlackRoad Automation SOP
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
This directory contains the **complete Standard Operating Procedures (SOPs)** for BlackRoad's automated ERP system, which integrates:
|
||||||
|
|
||||||
|
- **GitHub** (source control + CI/CD automation)
|
||||||
|
- **Salesforce** (customer data + business logic)
|
||||||
|
- **Asana** (project management + task tracking)
|
||||||
|
|
||||||
|
**Goal:** Automate the entire journey from "deal closed" to "code deployed" with minimal human intervention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### For Operators (Brenda)
|
||||||
|
|
||||||
|
**Start Here:** [Brenda's New Client Checklist](./playbooks/brenda-new-client-checklist.md)
|
||||||
|
|
||||||
|
This is your step-by-step guide for onboarding new clients. No technical knowledge required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### For Engineers
|
||||||
|
|
||||||
|
**Start Here:** [New Client Kickoff Workflow](./workflows/new-client-kickoff.md)
|
||||||
|
|
||||||
|
Understand how the automation works end-to-end.
|
||||||
|
|
||||||
|
**Then:** [Release Pipeline Workflow](./workflows/release-pipeline.md)
|
||||||
|
|
||||||
|
Learn how deployments trigger automatic updates across systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### For Salesforce Admins
|
||||||
|
|
||||||
|
**Start Here:**
|
||||||
|
1. [Salesforce Flow: Opportunity Automation](./salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
2. [Salesforce Orchestration: New Client Kickoff](./salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
|
||||||
|
These contain detailed specs for building the flows in Salesforce.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sop/
|
||||||
|
├── README.md ← You are here
|
||||||
|
│
|
||||||
|
├── workflows/ ← End-to-end process documentation
|
||||||
|
│ ├── new-client-kickoff.md ← Flagship workflow: 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 specs
|
||||||
|
│ ├── 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 workflow templates
|
||||||
|
│ │ ├── ci.yml ← Test, lint, build
|
||||||
|
│ │ ├── deploy.yml ← Deploy to Railway/Cloudflare
|
||||||
|
│ │ └── safety.yml ← Security scanning
|
||||||
|
│ └── repo-template/ ← Standard repo configuration
|
||||||
|
│ └── .github/
|
||||||
|
│ ├── pull_request_template.md
|
||||||
|
│ ├── labels.json
|
||||||
|
│ ├── branch-protection.md
|
||||||
|
│ └── ISSUE_TEMPLATE/
|
||||||
|
│ ├── bug_report.md
|
||||||
|
│ ├── feature_request.md
|
||||||
|
│ └── deployment_checklist.md
|
||||||
|
│
|
||||||
|
└── prompts/ ← Claude/Cece prompt seeds
|
||||||
|
└── (future: automation setup prompts)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Golden Path (How It All Works)
|
||||||
|
|
||||||
|
### 1. Salesforce: Deal Closes
|
||||||
|
|
||||||
|
**Human Action:** Sales marks Opportunity as "Closed Won"
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- Salesforce Flow creates Project record
|
||||||
|
- Orchestration kicks off (4 stages)
|
||||||
|
- Project Key generated (e.g., `ACME-1042`)
|
||||||
|
|
||||||
|
**Doc:** [Opp Automation Flow](./salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. GitHub: Repos Created
|
||||||
|
|
||||||
|
**Trigger:** Orchestration Stage 2 (Technical Setup)
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- Salesforce calls GitHub API
|
||||||
|
- 3 repos created:
|
||||||
|
- `blackroad-{PROJECT_KEY}-backend`
|
||||||
|
- `blackroad-{PROJECT_KEY}-frontend`
|
||||||
|
- `blackroad-{PROJECT_KEY}-ops`
|
||||||
|
- CI/CD workflows added
|
||||||
|
- Branch protection enabled
|
||||||
|
- Labels applied
|
||||||
|
- Secrets configured
|
||||||
|
|
||||||
|
**Doc:** [Salesforce → GitHub Integration](./integrations/salesforce-to-github.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Asana: Project Board Created
|
||||||
|
|
||||||
|
**Trigger:** Same Orchestration Stage 2
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- Salesforce calls Asana API
|
||||||
|
- Project created with sections:
|
||||||
|
- Discovery
|
||||||
|
- Architecture
|
||||||
|
- Build
|
||||||
|
- Testing
|
||||||
|
- Go-Live
|
||||||
|
- ~8-10 standard tasks created
|
||||||
|
- Tasks assigned with due dates
|
||||||
|
- Links back to Salesforce + GitHub
|
||||||
|
|
||||||
|
**Doc:** [Salesforce → Asana Integration](./integrations/salesforce-to-asana.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Engineers Work
|
||||||
|
|
||||||
|
**Human Action:** Engineers write code, create PRs, merge to `main`
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- GitHub Actions run CI pipeline (test + lint + build)
|
||||||
|
- PR must pass checks + get approval
|
||||||
|
- Merge triggers deploy pipeline
|
||||||
|
- Code deployed to Railway + Cloudflare
|
||||||
|
|
||||||
|
**Doc:** [Release Pipeline Workflow](./workflows/release-pipeline.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Feedback Loop
|
||||||
|
|
||||||
|
**Trigger:** Deploy succeeds
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- GitHub Actions call Salesforce API
|
||||||
|
- Update Project record:
|
||||||
|
- `Last_Deploy_At__c`
|
||||||
|
- `Last_Deploy_SHA__c`
|
||||||
|
- `Deploy_Status__c`
|
||||||
|
- Create Deployment record for audit
|
||||||
|
- Call Asana API to mark "Deploy to production" task complete
|
||||||
|
- Post deploy notification to Slack
|
||||||
|
|
||||||
|
**Doc:** [GitHub → Salesforce Integration](./integrations/github-to-salesforce.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Go-Live
|
||||||
|
|
||||||
|
**Human Action:** Customer Success does final walkthrough with client
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
- Project status updated to "Active"
|
||||||
|
- Go-live email sent automatically
|
||||||
|
- Renewal opportunity created
|
||||||
|
- Success metrics dashboard initialized
|
||||||
|
|
||||||
|
**Doc:** [New Client Kickoff Orchestration](./salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Week 1)
|
||||||
|
|
||||||
|
**Goal:** Get basic automation working
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create Salesforce custom objects (Project__c, Deployment__c)
|
||||||
|
- [ ] Set up Salesforce Named Credentials (GitHub, Asana)
|
||||||
|
- [ ] Create test GitHub repos manually
|
||||||
|
- [ ] Apply GitHub Actions workflows from templates
|
||||||
|
- [ ] Test Salesforce → GitHub API call manually
|
||||||
|
|
||||||
|
**Docs:**
|
||||||
|
- [Salesforce Flow Spec](./salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
- [Salesforce → GitHub Integration](./integrations/salesforce-to-github.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Core Workflow (Week 2-3)
|
||||||
|
|
||||||
|
**Goal:** Automate new client kickoff
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Build Salesforce Flow: Opp_Automation_OnStageChange
|
||||||
|
- [ ] Build Salesforce Orchestration: New_Client_Kickoff
|
||||||
|
- [ ] Implement GitHub repo creation (via Flow)
|
||||||
|
- [ ] Implement Asana project creation (via Flow)
|
||||||
|
- [ ] Test end-to-end with 1 test client
|
||||||
|
|
||||||
|
**Docs:**
|
||||||
|
- [New Client Kickoff Workflow](./workflows/new-client-kickoff.md)
|
||||||
|
- [Salesforce Orchestration Spec](./salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Feedback Loop (Week 4)
|
||||||
|
|
||||||
|
**Goal:** Close the loop with deploy notifications
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Add GitHub → Salesforce workflow to repos
|
||||||
|
- [ ] Test deploy updates Salesforce Project record
|
||||||
|
- [ ] Add GitHub → Asana integration (mark tasks complete)
|
||||||
|
- [ ] Set up Slack notifications
|
||||||
|
- [ ] Deploy to 3 pilot projects
|
||||||
|
|
||||||
|
**Docs:**
|
||||||
|
- [Release Pipeline Workflow](./workflows/release-pipeline.md)
|
||||||
|
- [GitHub → Salesforce Integration](./integrations/github-to-salesforce.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Scale (Week 5+)
|
||||||
|
|
||||||
|
**Goal:** Roll out to all new clients
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Train operations team on new process
|
||||||
|
- [ ] Document troubleshooting steps
|
||||||
|
- [ ] Create monitoring dashboard
|
||||||
|
- [ ] Roll out to all new deals
|
||||||
|
- [ ] Migrate existing clients gradually
|
||||||
|
|
||||||
|
**Docs:**
|
||||||
|
- [Brenda's Checklist](./playbooks/brenda-new-client-checklist.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
### 1. Event-Driven Everything
|
||||||
|
|
||||||
|
**Old Way:** "Brenda, can you create the repos and set up Asana?"
|
||||||
|
|
||||||
|
**New Way:** Mark opportunity as Closed Won → everything happens automatically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. GitHub-First Configuration
|
||||||
|
|
||||||
|
All workflows, templates, and configs live in **version control** (this repo).
|
||||||
|
|
||||||
|
Changes go through PR → review → merge → deploy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Two Views
|
||||||
|
|
||||||
|
**Operator View:** Simple checklists, no jargon, clear escalation paths
|
||||||
|
|
||||||
|
**Engineer View:** Detailed specs, API payloads, error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. No Manual Status Syncing
|
||||||
|
|
||||||
|
Status lives in **one place** (Salesforce Project record).
|
||||||
|
|
||||||
|
Everything else subscribes via API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Repos didn't get created after 15 minutes"
|
||||||
|
|
||||||
|
1. Check Salesforce debug logs for HTTP callout errors
|
||||||
|
2. Verify GitHub API credentials in Named Credential
|
||||||
|
3. Check GitHub App permissions
|
||||||
|
4. See: [Salesforce → GitHub Integration, Error Handling](./integrations/salesforce-to-github.md#error-handling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Asana project is missing tasks"
|
||||||
|
|
||||||
|
1. Check Salesforce debug logs
|
||||||
|
2. Verify Asana PAT is valid
|
||||||
|
3. Check custom metadata: Asana_Task_Template__mdt
|
||||||
|
4. See: [Salesforce → Asana Integration, Error Handling](./integrations/salesforce-to-asana.md#error-handling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Deploy didn't update Salesforce"
|
||||||
|
|
||||||
|
1. Check GitHub Actions workflow logs
|
||||||
|
2. Verify PROJECT_KEY was extracted correctly from repo name
|
||||||
|
3. Check Salesforce API credentials in GitHub secrets
|
||||||
|
4. See: [GitHub → Salesforce Integration, Error Handling](./integrations/github-to-salesforce.md#error-handling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "How do I report an automation bug?"
|
||||||
|
|
||||||
|
**In GitHub:**
|
||||||
|
1. Go to this repo
|
||||||
|
2. Create new issue
|
||||||
|
3. Use label: `automation-bug`
|
||||||
|
4. Include:
|
||||||
|
- Salesforce Project URL
|
||||||
|
- Expected vs. actual behavior
|
||||||
|
- Screenshots/logs
|
||||||
|
|
||||||
|
**In Slack:**
|
||||||
|
Post in #ops with:
|
||||||
|
- Project Key
|
||||||
|
- What broke
|
||||||
|
- Link to Salesforce Project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metrics & Monitoring
|
||||||
|
|
||||||
|
### Track These KPIs:
|
||||||
|
|
||||||
|
| Metric | Target | How to Measure |
|
||||||
|
|--------|--------|----------------|
|
||||||
|
| Time to First Commit | < 3 days | GitHub first commit - SF Start Date |
|
||||||
|
| Time to Go-Live | < 20 days | Go Live Date - Start Date |
|
||||||
|
| Automation Success Rate | > 95% | Projects with repos / Total projects |
|
||||||
|
| Manual Intervention Rate | < 10% | Projects needing fixes / Total |
|
||||||
|
| Deploy Frequency | > 5/week | Deploys per project per week |
|
||||||
|
| Deploy Success Rate | > 95% | Successful deploys / Total |
|
||||||
|
|
||||||
|
**Salesforce Reports:**
|
||||||
|
- "Projects by Status"
|
||||||
|
- "Deployments by Week"
|
||||||
|
- "Automation Errors (Last 30 Days)"
|
||||||
|
|
||||||
|
**GitHub Insights:**
|
||||||
|
- Actions usage
|
||||||
|
- Deploy frequency
|
||||||
|
- Build success rate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Team Roles
|
||||||
|
|
||||||
|
### Operations (Brenda)
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Mark deals as Closed Won
|
||||||
|
- Verify automation ran successfully
|
||||||
|
- Communicate with clients
|
||||||
|
- Monitor Asana project progress
|
||||||
|
- Escalate issues to engineering
|
||||||
|
|
||||||
|
**Primary Doc:** [Brenda's Checklist](./playbooks/brenda-new-client-checklist.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Salesforce Admin
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Build and maintain Flows + Orchestrations
|
||||||
|
- Manage Named Credentials
|
||||||
|
- Monitor API logs
|
||||||
|
- Troubleshoot Salesforce-side errors
|
||||||
|
|
||||||
|
**Primary Docs:**
|
||||||
|
- [Salesforce Flow Spec](./salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
- [Salesforce Orchestration Spec](./salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DevOps / Engineering
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Maintain GitHub Actions workflows
|
||||||
|
- Configure repos via automation
|
||||||
|
- Monitor deploy pipelines
|
||||||
|
- Troubleshoot GitHub/Railway/Cloudflare issues
|
||||||
|
|
||||||
|
**Primary Docs:**
|
||||||
|
- [Release Pipeline Workflow](./workflows/release-pipeline.md)
|
||||||
|
- [GitHub Actions Templates](./templates/github-actions/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Integration Engineer
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Maintain API integrations
|
||||||
|
- Monitor API logs and rate limits
|
||||||
|
- Update integration specs
|
||||||
|
- Handle authentication issues
|
||||||
|
|
||||||
|
**Primary Docs:**
|
||||||
|
- [Salesforce → GitHub Integration](./integrations/salesforce-to-github.md)
|
||||||
|
- [GitHub → Salesforce Integration](./integrations/github-to-salesforce.md)
|
||||||
|
- [Salesforce → Asana Integration](./integrations/salesforce-to-asana.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Compliance
|
||||||
|
|
||||||
|
### Credentials Management
|
||||||
|
|
||||||
|
**Salesforce:**
|
||||||
|
- Use Named Credentials (not hardcoded tokens)
|
||||||
|
- Rotate OAuth tokens quarterly
|
||||||
|
- Use encrypted custom settings for sensitive data
|
||||||
|
|
||||||
|
**GitHub:**
|
||||||
|
- Use GitHub App (not PAT) for production
|
||||||
|
- Rotate secrets every 90 days
|
||||||
|
- Use organization-level secrets where possible
|
||||||
|
|
||||||
|
**Asana:**
|
||||||
|
- Use dedicated integration PAT
|
||||||
|
- Don't share PAT across integrations
|
||||||
|
- Rotate every 90 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Audit Trail
|
||||||
|
|
||||||
|
**Track:**
|
||||||
|
- All API calls (Salesforce Custom Object: API_Log__c)
|
||||||
|
- All deployments (Salesforce: Deployment__c)
|
||||||
|
- All automation errors (Cases with Type = "Automation Bug")
|
||||||
|
|
||||||
|
**Review:**
|
||||||
|
- Weekly: Error logs
|
||||||
|
- Monthly: Success rates, anomalies
|
||||||
|
- Quarterly: Security audit, credential rotation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQs
|
||||||
|
|
||||||
|
### Q: What if I need to create a repo manually?
|
||||||
|
|
||||||
|
**A:** Follow the [Repo Template](./templates/repo-template/) to apply:
|
||||||
|
- Labels
|
||||||
|
- Branch protection
|
||||||
|
- Workflows
|
||||||
|
- PR template
|
||||||
|
- Issue templates
|
||||||
|
|
||||||
|
Then manually update Salesforce Project record with repo URLs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q: Can I customize the Asana tasks for different package types?
|
||||||
|
|
||||||
|
**A:** Yes! Edit the Custom Metadata Type: `Asana_Task_Template__mdt`
|
||||||
|
|
||||||
|
Add records with conditions based on Package_Type__c.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q: How do I add a new GitHub Actions workflow to all repos?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
1. Add workflow to [templates/github-actions/](./templates/github-actions/)
|
||||||
|
2. Update Salesforce Flow to include new workflow in repo creation
|
||||||
|
3. For existing repos, use a script or PR to all repos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q: What if a client wants a custom domain (not .blackroad.app)?
|
||||||
|
|
||||||
|
**A:** Update the `Primary_Domain__c` field in Salesforce, then:
|
||||||
|
1. Configure Cloudflare custom domain
|
||||||
|
2. Update environment variables in Railway
|
||||||
|
3. Redeploy frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This SOP is **living documentation**. If you:
|
||||||
|
- Find an error
|
||||||
|
- Want to improve a process
|
||||||
|
- Have a better way to do something
|
||||||
|
|
||||||
|
**Submit a PR!**
|
||||||
|
|
||||||
|
1. Edit the relevant `.md` file
|
||||||
|
2. Create a PR with clear description
|
||||||
|
3. Tag @ops or @devops for review
|
||||||
|
4. Merge once approved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
### Internal Support
|
||||||
|
|
||||||
|
**Slack Channels:**
|
||||||
|
- `#ops` - General operations questions
|
||||||
|
- `#dev` - Engineering / technical questions
|
||||||
|
- `#automation` - Automation bugs and improvements
|
||||||
|
|
||||||
|
**Email:**
|
||||||
|
- ops@blackroad.com - Operations team
|
||||||
|
- devops@blackroad.com - DevOps team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### External Resources
|
||||||
|
|
||||||
|
**Salesforce:**
|
||||||
|
- [Flow Builder Docs](https://help.salesforce.com/s/articleView?id=sf.flow.htm)
|
||||||
|
- [REST API Docs](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/)
|
||||||
|
|
||||||
|
**GitHub:**
|
||||||
|
- [Actions Docs](https://docs.github.com/en/actions)
|
||||||
|
- [REST API Docs](https://docs.github.com/en/rest)
|
||||||
|
|
||||||
|
**Asana:**
|
||||||
|
- [API Docs](https://developers.asana.com/docs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Date | Version | Change | Author |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| 2025-11-17 | 1.0 | Initial SOP created - complete automation system | Cece (Claude) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**If you're here to implement this:**
|
||||||
|
|
||||||
|
1. **Week 1:** Read [New Client Kickoff Workflow](./workflows/new-client-kickoff.md)
|
||||||
|
2. **Week 2:** Set up Salesforce objects + credentials
|
||||||
|
3. **Week 3:** Build the Salesforce Flow
|
||||||
|
4. **Week 4:** Test with 1 test client end-to-end
|
||||||
|
5. **Week 5:** Roll out to production
|
||||||
|
|
||||||
|
**If you're here to use this:**
|
||||||
|
|
||||||
|
- **Operators:** [Brenda's Checklist](./playbooks/brenda-new-client-checklist.md)
|
||||||
|
- **Engineers:** [Release Pipeline](./workflows/release-pipeline.md)
|
||||||
|
- **Salesforce Admins:** [Flow Spec](./salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Welcome to Automate The Company Day. Let's make it happen.** 🚀
|
||||||
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) |
|
||||||
347
sop/playbooks/brenda-new-client-checklist.md
Normal file
347
sop/playbooks/brenda-new-client-checklist.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# New Client Kickoff Checklist (Brenda Edition)
|
||||||
|
|
||||||
|
**For:** Operations, Customer Success, Account Managers
|
||||||
|
**Time:** 30 minutes of your time, 5-10 minutes of robot time
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
This is your **simple, step-by-step guide** for onboarding a new client after a deal closes. Most of the work happens automatically, but you need to:
|
||||||
|
|
||||||
|
1. Make sure everything kicks off correctly
|
||||||
|
2. Fill in a few details the robots can't know
|
||||||
|
3. Verify everything worked
|
||||||
|
4. Communicate with the customer
|
||||||
|
|
||||||
|
**No technical knowledge required.** If something breaks, there are clear instructions for who to ask for help.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before You Start
|
||||||
|
|
||||||
|
You'll need:
|
||||||
|
|
||||||
|
- [ ] Access to Salesforce
|
||||||
|
- [ ] Access to Asana
|
||||||
|
- [ ] Client's primary contact info (name, email, phone)
|
||||||
|
- [ ] Client's domain preference (if custom)
|
||||||
|
- [ ] Package type they purchased (OS / Console / Custom)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Checklist
|
||||||
|
|
||||||
|
### 📋 **Part 1: Mark the Deal Closed (Day 0)**
|
||||||
|
|
||||||
|
**Where:** Salesforce
|
||||||
|
|
||||||
|
1. Open the **Opportunity** record for this deal
|
||||||
|
2. Click **Edit**
|
||||||
|
3. Change **Stage** to: `Closed Won`
|
||||||
|
4. Fill in these fields if they're empty:
|
||||||
|
- **Account Name:** Company name
|
||||||
|
- **Primary Contact:** Main person at the company
|
||||||
|
- **Domain/Subdomain:** What they want their site to be called (e.g., `acme-portal`)
|
||||||
|
- **Package Type:** OS / Console / Custom (what they bought)
|
||||||
|
- **Service Tier:** Starter / Pro / Enterprise
|
||||||
|
- **Start Date:** Today (or agreed start date)
|
||||||
|
5. Click **Save**
|
||||||
|
|
||||||
|
**What happens next (automatically):**
|
||||||
|
- A **Project** record gets created in Salesforce (within 2 minutes)
|
||||||
|
- Repos appear in GitHub (within 5-10 minutes)
|
||||||
|
- An **Asana project** gets created with all tasks (within 5-10 minutes)
|
||||||
|
- You get a **Slack notification** in #ops saying everything is ready
|
||||||
|
|
||||||
|
**⏱️ Wait 10 minutes**, then move to Part 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ **Part 2: Verify the Magic Happened (Day 0)**
|
||||||
|
|
||||||
|
**Where:** Salesforce → Project record
|
||||||
|
|
||||||
|
1. Go back to the **Opportunity** in Salesforce
|
||||||
|
2. Scroll down to **Related** → **Projects**
|
||||||
|
3. You should see a new **Project** record with a name like `ACME-X7K9`
|
||||||
|
|
||||||
|
**Click into the Project record and check:**
|
||||||
|
|
||||||
|
- [ ] **Project Key** is filled in (e.g., `ACME-X7K9`)
|
||||||
|
- [ ] **Status** says: `Setup In Progress`
|
||||||
|
- [ ] **Backend Repo URL** is filled in (a GitHub link)
|
||||||
|
- [ ] **Frontend Repo URL** is filled in (a GitHub link)
|
||||||
|
- [ ] **Ops Repo URL** is filled in (a GitHub link)
|
||||||
|
- [ ] **Asana Project URL** is filled in (an Asana link)
|
||||||
|
|
||||||
|
**If any of these are empty after 15 minutes:**
|
||||||
|
→ Something broke. Skip to "Troubleshooting" section below.
|
||||||
|
|
||||||
|
**If everything is filled in:**
|
||||||
|
→ 🎉 You're golden! Move to Part 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 **Part 3: Add Client-Specific Details (Day 0)**
|
||||||
|
|
||||||
|
**Where:** Asana
|
||||||
|
|
||||||
|
1. Click the **Asana Project URL** from the Salesforce Project record (opens Asana)
|
||||||
|
2. You should see a project with sections: Discovery, Architecture, Build, Testing, Go-Live
|
||||||
|
3. Go to the **Discovery** section
|
||||||
|
4. Click the task: **"Confirm domain + DNS with client"**
|
||||||
|
5. Assign it to **yourself**
|
||||||
|
6. Add a comment with:
|
||||||
|
- Client's preferred domain/subdomain
|
||||||
|
- Any special requests (custom branding, integrations, etc.)
|
||||||
|
- Primary contact info
|
||||||
|
7. Click the task: **"Gather branding assets"**
|
||||||
|
8. Assign it to **Design team**
|
||||||
|
9. Add a comment with:
|
||||||
|
- "Client: [Company Name]"
|
||||||
|
- "Contact: [Name, Email]"
|
||||||
|
- "Need: Logo, brand colors, fonts"
|
||||||
|
|
||||||
|
**Where:** Salesforce Project
|
||||||
|
|
||||||
|
1. Go back to the **Project** record in Salesforce
|
||||||
|
2. Click **Edit**
|
||||||
|
3. Fill in:
|
||||||
|
- **Primary Contact:** Lookup the Contact record
|
||||||
|
- **Technical Owner:** Assign to a developer (or leave blank for now)
|
||||||
|
- **Customer Notes:** Any special context about this client
|
||||||
|
4. Click **Save**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📧 **Part 4: Notify the Client (Day 0-1)**
|
||||||
|
|
||||||
|
**Where:** Email / Phone
|
||||||
|
|
||||||
|
Send the client a friendly kickoff message:
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
|
||||||
|
> Subject: Welcome to BlackRoad! Your Project is Live 🚀
|
||||||
|
>
|
||||||
|
> Hi [Client Name],
|
||||||
|
>
|
||||||
|
> Great news! We've kicked off your project. Here's what's happening:
|
||||||
|
>
|
||||||
|
> **Your Project:** [Project Key, e.g., ACME-X7K9]
|
||||||
|
> **Package:** [OS / Console / Custom]
|
||||||
|
> **Next Steps:**
|
||||||
|
> 1. Our team will reach out within 24 hours to confirm your domain and branding.
|
||||||
|
> 2. We'll send you a staging link within 3-5 business days for initial review.
|
||||||
|
> 3. Your primary point of contact is [Technical Owner Name] for technical questions, and me for everything else.
|
||||||
|
>
|
||||||
|
> **Need anything?**
|
||||||
|
> Just reply to this email or call me at [your number].
|
||||||
|
>
|
||||||
|
> Looking forward to building something great together!
|
||||||
|
>
|
||||||
|
> [Your Name]
|
||||||
|
> [Your Title]
|
||||||
|
> BlackRoad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 👀 **Part 5: Monitor Progress (Ongoing)**
|
||||||
|
|
||||||
|
**Where:** Asana (check daily)
|
||||||
|
|
||||||
|
1. Open the **Asana project** for this client
|
||||||
|
2. Look at tasks:
|
||||||
|
- ✅ **Green checkmarks** = Done automatically or by the team
|
||||||
|
- ⏳ **In Progress** = Someone's working on it
|
||||||
|
- 🔴 **Overdue** = Needs attention
|
||||||
|
|
||||||
|
**What to watch for:**
|
||||||
|
|
||||||
|
- Tasks marked "blocked" → Check the comments, see what's blocking, help unblock
|
||||||
|
- Tasks overdue by > 2 days → Ping the assignee in Slack or Asana
|
||||||
|
- Client reaches out with questions → Add a task in Asana under "Discovery" section
|
||||||
|
|
||||||
|
**Where:** Salesforce Project (check weekly)
|
||||||
|
|
||||||
|
1. Open the **Project** record
|
||||||
|
2. Look at **Last Deploy At** field:
|
||||||
|
- Should update every few days (means engineers are deploying)
|
||||||
|
- If it's been > 1 week with no deploys → Ask in #ops "Is [Project Key] blocked?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎯 **Part 6: Final Review & Go-Live (Day 14-30)**
|
||||||
|
|
||||||
|
**Where:** Asana → "Go-Live" section
|
||||||
|
|
||||||
|
When you see the task **"Final client walkthrough"** assigned to you:
|
||||||
|
|
||||||
|
1. Schedule a 30-minute call with the client
|
||||||
|
2. Walk them through their staging environment
|
||||||
|
3. Get their final approval:
|
||||||
|
- [ ] Design looks good
|
||||||
|
- [ ] Functionality works as expected
|
||||||
|
- [ ] They're ready to go live
|
||||||
|
4. In the Asana task, add a comment: "Client approved on [Date]. Ready for production."
|
||||||
|
5. Assign the task **"Deploy to production"** to **DevOps**
|
||||||
|
|
||||||
|
**What happens next (automatically):**
|
||||||
|
- Engineers deploy to production
|
||||||
|
- Your Asana task auto-completes when deploy succeeds
|
||||||
|
- Salesforce updates with production URL
|
||||||
|
- You get a Slack notification: "✅ [Project Key] is live!"
|
||||||
|
|
||||||
|
**Where:** Email the client
|
||||||
|
|
||||||
|
Send the go-live notification:
|
||||||
|
|
||||||
|
> Subject: You're Live! 🎉
|
||||||
|
>
|
||||||
|
> Hi [Client Name],
|
||||||
|
>
|
||||||
|
> Exciting news – your BlackRoad site is now **live in production**!
|
||||||
|
>
|
||||||
|
> **Your URL:** https://[their-domain].blackroad.app
|
||||||
|
> **Login credentials:** [sent separately via secure method]
|
||||||
|
>
|
||||||
|
> **What's next:**
|
||||||
|
> - We'll monitor the site 24/7 for the first week
|
||||||
|
> - If you notice anything unusual, email support@blackroad.com or reply here
|
||||||
|
> - We'll check in with you in 1 week to see how things are going
|
||||||
|
>
|
||||||
|
> Congratulations! 🚀
|
||||||
|
>
|
||||||
|
> [Your Name]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: "Project record was created, but repos/Asana are empty after 15 minutes"
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
1. Go to the **Project** record in Salesforce
|
||||||
|
2. Copy the **Project Key** (e.g., `ACME-X7K9`)
|
||||||
|
3. Go to GitHub: https://github.com/blackboxprogramming
|
||||||
|
4. Search for repos with that Project Key in the name
|
||||||
|
5. **If repos exist but URLs aren't in Salesforce:**
|
||||||
|
- Manually copy the repo URLs into the Salesforce Project fields
|
||||||
|
- Post in #ops: "Automation hiccup for [Project Key] – repos created but didn't sync to Salesforce"
|
||||||
|
6. **If repos DON'T exist:**
|
||||||
|
- Post in #ops: "Urgent: GitHub repos not created for [Project Key]. Need manual setup."
|
||||||
|
- Tag @devops
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problem: "Asana project was never created"
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
1. Manually create an Asana project:
|
||||||
|
- Go to Asana
|
||||||
|
- Click **+ New Project**
|
||||||
|
- Name it: `[Account Name] - [Project Key]`
|
||||||
|
- Choose **Board** view
|
||||||
|
2. Copy the project URL
|
||||||
|
3. Paste it into the Salesforce **Project** record → **Asana Project URL** field
|
||||||
|
4. Add these sections manually:
|
||||||
|
- Discovery
|
||||||
|
- Architecture
|
||||||
|
- Build
|
||||||
|
- Testing
|
||||||
|
- Go-Live
|
||||||
|
5. Post in #ops: "Asana automation failed for [Project Key] – created manually"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problem: "Engineers are asking me technical questions I don't understand"
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
1. **Don't guess.** It's okay to say "I don't know, let me find out."
|
||||||
|
2. Ask the client for clarification
|
||||||
|
3. Post the question + client's answer in the **Asana project** under the relevant task
|
||||||
|
4. Tag the engineer who asked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problem: "Client is frustrated / things are taking too long"
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
1. Look at the **Asana project** → find which tasks are overdue
|
||||||
|
2. Post in #ops: "[Project Key] is delayed – [Task Name] is overdue. Can someone help?"
|
||||||
|
3. Schedule a call with the client to explain:
|
||||||
|
- What's blocking us
|
||||||
|
- New timeline
|
||||||
|
- What we're doing to unblock
|
||||||
|
4. Follow up in **Salesforce** → Project record → add a note in **Customer Notes**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
**Tip 1: Check Asana every morning**
|
||||||
|
Spend 5 minutes scanning all your active client projects. Catch issues early.
|
||||||
|
|
||||||
|
**Tip 2: Use Slack for quick questions**
|
||||||
|
If a task is blocked, post in #ops or #dev with the Asana task link. Much faster than email.
|
||||||
|
|
||||||
|
**Tip 3: Keep clients in the loop**
|
||||||
|
Send a quick "Hey, we deployed X this week" update every Friday. Clients love visibility.
|
||||||
|
|
||||||
|
**Tip 4: Use the Salesforce Activity feed**
|
||||||
|
Log every client call, email, or decision in Salesforce. Future-you will thank you.
|
||||||
|
|
||||||
|
**Tip 5: Trust the automation**
|
||||||
|
The robots are good at their job. If something doesn't auto-complete, it's probably because it's waiting on something manual (like your approval or client input). Check the task comments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| I need to... | Go here... |
|
||||||
|
|-------------|-----------|
|
||||||
|
| Mark a deal closed | Salesforce → Opportunity → Change Stage to "Closed Won" |
|
||||||
|
| Check if automation worked | Salesforce → Project record → Check if URLs are filled |
|
||||||
|
| See what tasks need doing | Asana → Open the project for that client |
|
||||||
|
| Find GitHub repos | Salesforce → Project record → Click the repo URL links |
|
||||||
|
| See latest deploys | Salesforce → Project record → "Last Deploy At" field |
|
||||||
|
| Report a broken automation | Slack → #ops → Tag @devops |
|
||||||
|
| Ask a technical question | Slack → #dev → Include Asana task link |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Who to Ask for Help
|
||||||
|
|
||||||
|
| If you need... | Ask... | Where... |
|
||||||
|
|---------------|--------|----------|
|
||||||
|
| Automation isn't working | @devops | Slack #ops |
|
||||||
|
| Task is blocked / unclear | The task assignee | Asana comment or Slack |
|
||||||
|
| Client has technical questions | @technical-owner (from Salesforce Project) | Slack or tag in Asana |
|
||||||
|
| Client is unhappy / escalation | Your manager | Slack DM or meeting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
**You are the glue between the client and the robots.**
|
||||||
|
|
||||||
|
Your job is NOT to understand how GitHub Actions work or what a "CI pipeline" is.
|
||||||
|
|
||||||
|
Your job IS to:
|
||||||
|
- Make sure clients feel heard and informed
|
||||||
|
- Catch things that fall through the cracks
|
||||||
|
- Keep Asana and Salesforce up to date with client context
|
||||||
|
- Escalate technical issues to technical people
|
||||||
|
|
||||||
|
**The system is designed to make your life easier.** If it's not, tell us and we'll fix it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?**
|
||||||
|
Post in #ops or email ops@blackroad.com
|
||||||
463
sop/salesforce/flows/opp-automation-onstagechange.md
Normal file
463
sop/salesforce/flows/opp-automation-onstagechange.md
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
# Salesforce Flow: Opportunity Automation on Stage Change
|
||||||
|
|
||||||
|
**API Name:** `Opp_Automation_OnStageChange`
|
||||||
|
**Type:** Record-Triggered Flow (After Save)
|
||||||
|
**Object:** Opportunity
|
||||||
|
**Status:** Active
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This flow automates the creation of Project records and kickoff of the orchestration when an Opportunity moves to "Closed Won" stage.
|
||||||
|
|
||||||
|
**Business Value:**
|
||||||
|
- Eliminates manual project setup
|
||||||
|
- Ensures consistency across all new clients
|
||||||
|
- Triggers downstream automation (GitHub, Asana)
|
||||||
|
- Captures all required metadata at deal closure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trigger Configuration
|
||||||
|
|
||||||
|
### Object
|
||||||
|
`Opportunity`
|
||||||
|
|
||||||
|
### Trigger Type
|
||||||
|
**Record-Triggered Flow (After Save)**
|
||||||
|
|
||||||
|
### Trigger Criteria
|
||||||
|
**When:** A record is updated
|
||||||
|
**Condition Requirements:**
|
||||||
|
- **StageName** changed
|
||||||
|
- **StageName** equals "Closed Won"
|
||||||
|
|
||||||
|
**Entry Condition Logic:**
|
||||||
|
```
|
||||||
|
AND(
|
||||||
|
ISCHANGED(StageName),
|
||||||
|
TEXT(StageName) = "Closed Won"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimization
|
||||||
|
- ✅ Run asynchronously (Fast Field Updates) - **No** (need to create related records)
|
||||||
|
- ✅ Trigger order - **1** (run first, before other flows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow Variables
|
||||||
|
|
||||||
|
Create these variables at the start:
|
||||||
|
|
||||||
|
| Variable Name | Type | Default Value | Description |
|
||||||
|
|---------------|------|---------------|-------------|
|
||||||
|
| `varProjectKey` | Text | null | Generated project key (e.g., ACME-X7K9) |
|
||||||
|
| `varAccountSlug` | Text | null | Account name converted to slug |
|
||||||
|
| `varRandomSuffix` | Text | null | 4-character random suffix |
|
||||||
|
| `varProjectRecord` | Record (Project__c) | null | Created project record |
|
||||||
|
| `varExistingProject` | Record Collection (Project__c) | null | Check for existing project |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow Logic
|
||||||
|
|
||||||
|
### 1. Decision: Validate Required Fields
|
||||||
|
|
||||||
|
**Element Type:** Decision
|
||||||
|
|
||||||
|
**Criteria:**
|
||||||
|
Check if all required fields are populated:
|
||||||
|
|
||||||
|
```
|
||||||
|
AND(
|
||||||
|
NOT(ISBLANK({!$Record.AccountId})),
|
||||||
|
NOT(ISBLANK({!$Record.Primary_Domain__c})),
|
||||||
|
NOT(ISBLANK({!$Record.Package_Type__c})),
|
||||||
|
NOT(ISBLANK({!$Record.Service_Tier__c}))
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outcomes:**
|
||||||
|
- **Valid** → Continue to next step
|
||||||
|
- **Invalid** → Send error notification and stop
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2A. Path: Invalid - Send Error Notification
|
||||||
|
|
||||||
|
**Element Type:** Action - Send Email
|
||||||
|
|
||||||
|
**To:** Opportunity Owner
|
||||||
|
**Subject:** `Action Required: Opportunity {!$Record.Name} Missing Information`
|
||||||
|
**Body:**
|
||||||
|
```
|
||||||
|
The opportunity "{!$Record.Name}" was marked as Closed Won, but is missing required information:
|
||||||
|
|
||||||
|
Required Fields:
|
||||||
|
- Account
|
||||||
|
- Primary Domain
|
||||||
|
- Package Type
|
||||||
|
- Service Tier
|
||||||
|
|
||||||
|
Please update the opportunity and change the stage to "Closed Won" again to trigger automation.
|
||||||
|
|
||||||
|
Opportunity Link: {!$Record.Id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Then:** Stop
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2B. Path: Valid - Continue
|
||||||
|
|
||||||
|
#### Step 2.1: Check for Existing Project
|
||||||
|
|
||||||
|
**Element Type:** Get Records
|
||||||
|
|
||||||
|
**Object:** `Project__c`
|
||||||
|
**Conditions:**
|
||||||
|
- `Opportunity__c` equals `{!$Record.Id}`
|
||||||
|
|
||||||
|
**Store Output:** `varExistingProject`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.2: Decision: Project Already Exists?
|
||||||
|
|
||||||
|
**Criteria:**
|
||||||
|
```
|
||||||
|
NOT(ISNULL({!varExistingProject}))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outcomes:**
|
||||||
|
- **Exists** → Skip creation, update existing
|
||||||
|
- **Does Not Exist** → Create new project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.3A: Path: Project Exists - Update It
|
||||||
|
|
||||||
|
**Element Type:** Update Records
|
||||||
|
|
||||||
|
**Record:** `{!varExistingProject[0]}`
|
||||||
|
**Fields to Update:**
|
||||||
|
- `Status__c` = "Setup In Progress"
|
||||||
|
- `Updated_From_Opp__c` = `TODAY()`
|
||||||
|
- `Service_Tier__c` = `{!$Record.Service_Tier__c}`
|
||||||
|
- `Package_Type__c` = `{!$Record.Package_Type__c}`
|
||||||
|
|
||||||
|
**Then:** Skip to Step 3 (Launch Orchestration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.3B: Path: Project Does Not Exist - Create It
|
||||||
|
|
||||||
|
##### Step 2.3B.1: Generate Account Slug
|
||||||
|
|
||||||
|
**Element Type:** Assignment
|
||||||
|
|
||||||
|
**Variable:** `varAccountSlug`
|
||||||
|
**Operator:** Equals
|
||||||
|
**Value:** Formula:
|
||||||
|
```
|
||||||
|
SUBSTITUTE(
|
||||||
|
SUBSTITUTE(
|
||||||
|
SUBSTITUTE(
|
||||||
|
UPPER(LEFT({!$Record.Account.Name}, 10)),
|
||||||
|
" ", "-"
|
||||||
|
),
|
||||||
|
".", ""
|
||||||
|
),
|
||||||
|
",", ""
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** Convert "Acme Corp." → "ACME-CORP"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Step 2.3B.2: Generate Random Suffix
|
||||||
|
|
||||||
|
**Element Type:** Assignment
|
||||||
|
|
||||||
|
**Variable:** `varRandomSuffix`
|
||||||
|
**Value:** Formula:
|
||||||
|
```
|
||||||
|
SUBSTITUTE(
|
||||||
|
LEFT(TEXT(RAND()), 4),
|
||||||
|
"0.", ""
|
||||||
|
) & TEXT(FLOOR(RAND() * 10000))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Generates a 4-character alphanumeric suffix (simplified version; in production, use a more robust random generator or auto-number)
|
||||||
|
|
||||||
|
**Better Alternative:** Use Salesforce auto-number field on Project__c:
|
||||||
|
- Field: `Project_Number__c` (Auto Number)
|
||||||
|
- Format: `{0000}`
|
||||||
|
- Starting Number: `1000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Step 2.3B.3: Build Project Key
|
||||||
|
|
||||||
|
**Element Type:** Assignment
|
||||||
|
|
||||||
|
**Variable:** `varProjectKey`
|
||||||
|
**Value:**
|
||||||
|
```
|
||||||
|
{!varAccountSlug} & "-" & TEXT({!$Record.Project_Number__c})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Output:** `ACME-1042`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Step 2.3B.4: Create Project Record
|
||||||
|
|
||||||
|
**Element Type:** Create Records
|
||||||
|
|
||||||
|
**Object:** `Project__c`
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field API Name | Value |
|
||||||
|
|----------------|-------|
|
||||||
|
| `Name` | `{!$Record.Account.Name} - {!varProjectKey}` |
|
||||||
|
| `Project_Key__c` | `{!varProjectKey}` |
|
||||||
|
| `Account__c` | `{!$Record.AccountId}` |
|
||||||
|
| `Opportunity__c` | `{!$Record.Id}` |
|
||||||
|
| `Primary_Domain__c` | `{!$Record.Primary_Domain__c}` |
|
||||||
|
| `Package_Type__c` | `{!$Record.Package_Type__c}` |
|
||||||
|
| `Service_Tier__c` | `{!$Record.Service_Tier__c}` |
|
||||||
|
| `Start_Date__c` | `TODAY()` |
|
||||||
|
| `Status__c` | `"Setup In Progress"` |
|
||||||
|
| `Technical_Owner__c` | `{!$Record.OwnerId}` (or default to a user) |
|
||||||
|
| `OwnerId` | `{!$Record.OwnerId}` |
|
||||||
|
|
||||||
|
**Store Output:** `varProjectRecord`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Launch Orchestration
|
||||||
|
|
||||||
|
**Element Type:** Action - Orchestration
|
||||||
|
|
||||||
|
**Orchestration Name:** `New_Client_Kickoff_Orchestration`
|
||||||
|
|
||||||
|
**Input Variables:**
|
||||||
|
- `ProjectRecordId` = `{!varProjectRecord.Id}`
|
||||||
|
- `OpportunityRecordId` = `{!$Record.Id}`
|
||||||
|
- `AccountRecordId` = `{!$Record.AccountId}`
|
||||||
|
|
||||||
|
**Run Asynchronously:** Yes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Send Notification to Ops Team
|
||||||
|
|
||||||
|
**Element Type:** Action - Post to Chatter / Slack
|
||||||
|
|
||||||
|
**Option A: Chatter Post**
|
||||||
|
```
|
||||||
|
New project created! 🚀
|
||||||
|
|
||||||
|
Project: {!varProjectRecord.Name}
|
||||||
|
Project Key: {!varProjectRecord.Project_Key__c}
|
||||||
|
Account: {!$Record.Account.Name}
|
||||||
|
Package: {!$Record.Package_Type__c}
|
||||||
|
|
||||||
|
Orchestration started. GitHub repos and Asana project will be created within 5-10 minutes.
|
||||||
|
|
||||||
|
Link: {!varProjectRecord.Id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Slack (via HTTP Callout)**
|
||||||
|
|
||||||
|
See [Integration: Salesforce → Slack](../../integrations/salesforce-to-slack.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Update Opportunity with Project Link
|
||||||
|
|
||||||
|
**Element Type:** Update Records
|
||||||
|
|
||||||
|
**Record:** `{!$Record}`
|
||||||
|
**Fields to Update:**
|
||||||
|
- `Project__c` (Lookup field) = `{!varProjectRecord.Id}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Fault Path
|
||||||
|
|
||||||
|
If any step fails:
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Case
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `Subject` = `"Automation Error: {!$Record.Name} Project Creation Failed"`
|
||||||
|
- `Description` = `"Error creating project for opportunity: {!$Record.Id}\n\nError: {!$Flow.FaultMessage}"`
|
||||||
|
- `Priority` = `"High"`
|
||||||
|
- `Type` = `"Automation Bug"`
|
||||||
|
- `OwnerId` = `<DevOps Queue ID>`
|
||||||
|
|
||||||
|
**Then:** Send email to DevOps team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before activating:
|
||||||
|
|
||||||
|
- [ ] Test with valid opportunity (all fields populated)
|
||||||
|
- [ ] Test with missing fields (verify error email)
|
||||||
|
- [ ] Test with existing project (verify update path)
|
||||||
|
- [ ] Verify project key generation (unique and correct format)
|
||||||
|
- [ ] Verify orchestration kicks off
|
||||||
|
- [ ] Check Chatter/Slack notification appears
|
||||||
|
- [ ] Verify fault path (intentionally cause error)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Salesforce Objects & Fields
|
||||||
|
|
||||||
|
### Opportunity Custom Fields
|
||||||
|
|
||||||
|
Add these to the Opportunity object if they don't exist:
|
||||||
|
|
||||||
|
| Field API Name | Type | Picklist Values | Description |
|
||||||
|
|----------------|------|-----------------|-------------|
|
||||||
|
| `Primary_Domain__c` | Text(80) | N/A | Client's desired subdomain |
|
||||||
|
| `Package_Type__c` | Picklist | OS, Console, Custom | Product package purchased |
|
||||||
|
| `Service_Tier__c` | Picklist | Starter, Pro, Enterprise | Service level |
|
||||||
|
| `Project__c` | Lookup(Project__c) | N/A | Link to created project |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Project Custom Object
|
||||||
|
|
||||||
|
Create this custom object:
|
||||||
|
|
||||||
|
**API Name:** `Project__c`
|
||||||
|
**Label:** Project
|
||||||
|
**Plural Label:** Projects
|
||||||
|
**Record Name:** Project Name (Text)
|
||||||
|
|
||||||
|
#### Fields:
|
||||||
|
|
||||||
|
| Field API Name | Type | Length/Format | Unique | Required | Description |
|
||||||
|
|----------------|------|---------------|--------|----------|-------------|
|
||||||
|
| `Project_Key__c` | Text | 20 | ✅ Yes | ✅ Yes | Unique project identifier (e.g., ACME-1042) |
|
||||||
|
| `Account__c` | Lookup(Account) | N/A | ❌ | ✅ | Related account |
|
||||||
|
| `Opportunity__c` | Lookup(Opportunity) | N/A | ❌ | ✅ | Related opportunity |
|
||||||
|
| `Primary_Domain__c` | Text | 80 | ❌ | ✅ | Client's subdomain |
|
||||||
|
| `Package_Type__c` | Picklist | N/A | ❌ | ✅ | OS, Console, Custom |
|
||||||
|
| `Service_Tier__c` | Picklist | N/A | ❌ | ✅ | Starter, Pro, Enterprise |
|
||||||
|
| `Start_Date__c` | Date | N/A | ❌ | ✅ | Project start date |
|
||||||
|
| `Status__c` | Picklist | N/A | ❌ | ✅ | Setup In Progress, Active, On Hold, Completed |
|
||||||
|
| `Technical_Owner__c` | Lookup(User) | N/A | ❌ | ❌ | Lead developer |
|
||||||
|
| `Backend_Repo_URL__c` | URL | 255 | ❌ | ❌ | GitHub backend repo |
|
||||||
|
| `Frontend_Repo_URL__c` | URL | 255 | ❌ | ❌ | GitHub frontend repo |
|
||||||
|
| `Ops_Repo_URL__c` | URL | 255 | ❌ | ❌ | GitHub ops repo |
|
||||||
|
| `Asana_Project_URL__c` | URL | 255 | ❌ | ❌ | Asana project |
|
||||||
|
| `Last_Deploy_At__c` | DateTime | N/A | ❌ | ❌ | Last deployment timestamp |
|
||||||
|
| `Last_Deploy_SHA__c` | Text | 40 | ❌ | ❌ | Git commit SHA |
|
||||||
|
| `Last_Deploy_Branch__c` | Text | 100 | ❌ | ❌ | Git branch |
|
||||||
|
| `Last_Deploy_Actor__c` | Text | 100 | ❌ | ❌ | Who deployed |
|
||||||
|
| `Deploy_Status__c` | Picklist | N/A | ❌ | ❌ | Success, Failed, Rollback |
|
||||||
|
| `Environment__c` | Picklist | N/A | ❌ | ❌ | Staging, Production |
|
||||||
|
| `Backend_URL__c` | URL | 255 | ❌ | ❌ | Production backend URL |
|
||||||
|
| `Frontend_URL__c` | URL | 255 | ❌ | ❌ | Production frontend URL |
|
||||||
|
| `Release_Notes_URL__c` | URL | 255 | ❌ | ❌ | Link to latest release notes |
|
||||||
|
|
||||||
|
#### Page Layouts:
|
||||||
|
|
||||||
|
Create page layout with sections:
|
||||||
|
1. **Project Information** (Name, Project Key, Status, Dates)
|
||||||
|
2. **Client Details** (Account, Opportunity, Primary Domain, Package, Tier)
|
||||||
|
3. **Technical Details** (Technical Owner, Repo URLs, Asana URL)
|
||||||
|
4. **Deployment Info** (Last Deploy fields, URLs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Instructions
|
||||||
|
|
||||||
|
### Step 1: Create Custom Objects & Fields
|
||||||
|
|
||||||
|
Use Salesforce Setup → Object Manager to create:
|
||||||
|
- Project__c object with all fields listed above
|
||||||
|
- Custom fields on Opportunity object
|
||||||
|
|
||||||
|
### Step 2: Create Named Credentials (for later integrations)
|
||||||
|
|
||||||
|
Setup → Named Credentials:
|
||||||
|
- `GitHub_API` (for calling GitHub API)
|
||||||
|
- `Asana_API` (for calling Asana API)
|
||||||
|
- `Slack_Webhook` (for Slack notifications)
|
||||||
|
|
||||||
|
### Step 3: Build the Flow
|
||||||
|
|
||||||
|
1. Setup → Flows → New Flow
|
||||||
|
2. Choose "Record-Triggered Flow"
|
||||||
|
3. Configure trigger as specified above
|
||||||
|
4. Build each element in order
|
||||||
|
5. Save as "Opp_Automation_OnStageChange"
|
||||||
|
6. Activate
|
||||||
|
|
||||||
|
### Step 4: Test in Sandbox
|
||||||
|
|
||||||
|
1. Create a test opportunity
|
||||||
|
2. Fill all required fields
|
||||||
|
3. Change stage to "Closed Won"
|
||||||
|
4. Verify:
|
||||||
|
- Project record created
|
||||||
|
- Fields populated correctly
|
||||||
|
- Notification sent
|
||||||
|
- No errors in debug logs
|
||||||
|
|
||||||
|
### Step 5: Deploy to Production
|
||||||
|
|
||||||
|
Use Change Sets or Salesforce CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sf project deploy start \
|
||||||
|
--source-dir force-app/main/default/flows/Opp_Automation_OnStageChange.flow-meta.xml \
|
||||||
|
--target-org production
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Maintenance
|
||||||
|
|
||||||
|
### Weekly:
|
||||||
|
- Review flow error logs
|
||||||
|
- Check for opportunities stuck in "Closed Won" without projects
|
||||||
|
|
||||||
|
### Monthly:
|
||||||
|
- Review project key uniqueness
|
||||||
|
- Audit project creation success rate (target: >98%)
|
||||||
|
|
||||||
|
### Quarterly:
|
||||||
|
- Review and update picklist values
|
||||||
|
- Optimize flow performance if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Salesforce Orchestration: New Client Kickoff](../orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
- [Workflow: New Client Kickoff](../../workflows/new-client-kickoff.md)
|
||||||
|
- [Integration: Salesforce → GitHub](../../integrations/salesforce-to-github.md)
|
||||||
|
- [Integration: Salesforce → Asana](../../integrations/salesforce-to-asana.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Date | Version | Change | Author |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| 2025-11-17 | 1.0 | Initial specification | Cece (Claude) |
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
# Salesforce Orchestration: New Client Kickoff
|
||||||
|
|
||||||
|
**API Name:** `New_Client_Kickoff_Orchestration`
|
||||||
|
**Type:** Multi-Stage Orchestration
|
||||||
|
**Status:** Active
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This orchestration coordinates the entire **end-to-end new client onboarding process** across multiple teams and systems, from deal closure to go-live.
|
||||||
|
|
||||||
|
**Key Benefits:**
|
||||||
|
- Orchestrates cross-functional work (Sales, Engineering, Customer Success)
|
||||||
|
- Enforces process consistency
|
||||||
|
- Provides visibility into project status
|
||||||
|
- Automates hand-offs between teams
|
||||||
|
- Ensures nothing falls through the cracks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input Variables
|
||||||
|
|
||||||
|
| Variable Name | Type | Required | Description |
|
||||||
|
|---------------|------|----------|-------------|
|
||||||
|
| `ProjectRecordId` | Record ID | ✅ | ID of the Project__c record |
|
||||||
|
| `OpportunityRecordId` | Record ID | ✅ | ID of the Opportunity |
|
||||||
|
| `AccountRecordId` | Record ID | ✅ | ID of the Account |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stages Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Sales Handoff (1-2 days)
|
||||||
|
↓
|
||||||
|
2. Technical Setup (3-5 days) ← KEY AUTOMATION STAGE
|
||||||
|
↓
|
||||||
|
3. Customer Onboarding (5-10 days)
|
||||||
|
↓
|
||||||
|
4. Review & Go-Live (2-3 days)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total Timeline:** 11-20 days (varies by package complexity)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1: Sales Handoff
|
||||||
|
|
||||||
|
**Owner:** Sales Operations
|
||||||
|
**Duration:** 1-2 days
|
||||||
|
**Status Field:** `Project__c.Status__c` = "Sales Handoff"
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 1.1: Send Welcome Email to Client
|
||||||
|
|
||||||
|
**Element Type:** Action - Send Email
|
||||||
|
|
||||||
|
**Template:** `Client_Welcome_Email`
|
||||||
|
|
||||||
|
**To:** Primary Contact from Opportunity
|
||||||
|
**CC:** Account Executive, Account Owner
|
||||||
|
**From:** ops@blackroad.com
|
||||||
|
|
||||||
|
**Merge Fields:**
|
||||||
|
- `{!Account.Name}`
|
||||||
|
- `{!Project.Project_Key__c}`
|
||||||
|
- `{!Project.Package_Type__c}`
|
||||||
|
- `{!Opportunity.Owner.Name}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 1.2: Create Internal Kickoff Task
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Task
|
||||||
|
|
||||||
|
**Assigned To:** Sales Operations Manager
|
||||||
|
**Subject:** `Internal Kickoff: {!Project.Name}`
|
||||||
|
**Due Date:** `TODAY() + 1`
|
||||||
|
**Priority:** High
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
```
|
||||||
|
New project needs kickoff coordination:
|
||||||
|
|
||||||
|
Project: {!Project.Name}
|
||||||
|
Project Key: {!Project.Project_Key__c}
|
||||||
|
Account: {!Account.Name}
|
||||||
|
Package: {!Project.Package_Type__c}
|
||||||
|
Service Tier: {!Project.Service_Tier__c}
|
||||||
|
|
||||||
|
Action Required:
|
||||||
|
- Verify all Opportunity fields are complete
|
||||||
|
- Confirm primary contact is correct
|
||||||
|
- Schedule kickoff call with client
|
||||||
|
- Brief technical team on any special requirements
|
||||||
|
|
||||||
|
Salesforce Project: {!Project.Id}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 1.3: Wait for Approval
|
||||||
|
|
||||||
|
**Element Type:** Interactive Step - Screen Flow
|
||||||
|
|
||||||
|
**Screen:** "Sales Handoff Complete?"
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- Checkbox: "Kickoff call scheduled"
|
||||||
|
- Checkbox: "Client expectations set"
|
||||||
|
- Checkbox: "Special requirements documented"
|
||||||
|
- Text Area: "Notes for technical team"
|
||||||
|
|
||||||
|
**Assigned To:** Sales Operations Manager
|
||||||
|
|
||||||
|
**Completion Criteria:** All checkboxes checked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 1.4: Update Project Status
|
||||||
|
|
||||||
|
**Element Type:** Action - Update Records
|
||||||
|
|
||||||
|
**Record:** Project__c
|
||||||
|
**Fields:**
|
||||||
|
- `Status__c` = "Technical Setup"
|
||||||
|
- `Sales_Handoff_Completed__c` = `TODAY()`
|
||||||
|
- `Technical_Notes__c` = `{!Screen.Notes}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2: Technical Setup
|
||||||
|
|
||||||
|
**Owner:** DevOps / Engineering
|
||||||
|
**Duration:** 3-5 days (mostly automated)
|
||||||
|
**Status Field:** `Project__c.Status__c` = "Technical Setup"
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 2.1: Create GitHub Repositories
|
||||||
|
|
||||||
|
**Element Type:** Autolaunched Flow - HTTP Callout
|
||||||
|
|
||||||
|
**Named Credential:** `GitHub_API`
|
||||||
|
**Endpoint:** `POST https://api.github.com/orgs/blackboxprogramming/repos`
|
||||||
|
|
||||||
|
**For Each Repo Type:** (Backend, Frontend, Ops)
|
||||||
|
|
||||||
|
**Payload:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "blackroad-{!Project.Project_Key__c}-{!RepoType}",
|
||||||
|
"description": "{!Account.Name} - {!Project.Package_Type__c}",
|
||||||
|
"private": true,
|
||||||
|
"auto_init": true,
|
||||||
|
"gitignore_template": "Python"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Store Response:** Capture repo URLs
|
||||||
|
|
||||||
|
**Update Project Record:**
|
||||||
|
- `Backend_Repo_URL__c` = Response.html_url (for backend)
|
||||||
|
- `Frontend_Repo_URL__c` = Response.html_url (for frontend)
|
||||||
|
- `Ops_Repo_URL__c` = Response.html_url (for ops)
|
||||||
|
|
||||||
|
**See:** [Integration: Salesforce → GitHub](../../integrations/salesforce-to-github.md) for detailed API specs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.2: Apply Repository Templates
|
||||||
|
|
||||||
|
**Element Type:** Autolaunched Flow - HTTP Callout (Multiple)
|
||||||
|
|
||||||
|
**For Each Repo:**
|
||||||
|
|
||||||
|
1. **Create Labels**
|
||||||
|
- POST to `/repos/{owner}/{repo}/labels`
|
||||||
|
- Apply standard label set from `labels.json`
|
||||||
|
|
||||||
|
2. **Apply Branch Protection**
|
||||||
|
- PUT to `/repos/{owner}/{repo}/branches/main/protection`
|
||||||
|
- Configure as per branch-protection spec
|
||||||
|
|
||||||
|
3. **Create Workflows**
|
||||||
|
- POST to create files via GitHub API
|
||||||
|
- Add `ci.yml`, `deploy.yml`, `safety.yml`
|
||||||
|
|
||||||
|
4. **Add Secrets**
|
||||||
|
- POST to `/repos/{owner}/{repo}/actions/secrets`
|
||||||
|
- Add: `PROJECT_KEY`, `SALESFORCE_INSTANCE_URL`, `SALESFORCE_ACCESS_TOKEN`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.3: Create Asana Project
|
||||||
|
|
||||||
|
**Element Type:** Autolaunched Flow - HTTP Callout
|
||||||
|
|
||||||
|
**Named Credential:** `Asana_API`
|
||||||
|
**Endpoint:** `POST https://app.asana.com/api/1.0/projects`
|
||||||
|
|
||||||
|
**Payload:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"name": "{!Account.Name} - {!Project.Project_Key__c}",
|
||||||
|
"workspace": "{!$Credential.Asana_API.Workspace_GID}",
|
||||||
|
"team": "{!$Credential.Asana_API.Team_GID}",
|
||||||
|
"notes": "Salesforce Project: {!Project.Id}\nRepos:\n- Backend: {!Project.Backend_Repo_URL__c}\n- Frontend: {!Project.Frontend_Repo_URL__c}\n- Ops: {!Project.Ops_Repo_URL__c}",
|
||||||
|
"default_view": "board",
|
||||||
|
"color": "light-green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Store Response:** `{!AsanaProjectGID}`
|
||||||
|
|
||||||
|
**Update Project:**
|
||||||
|
- `Asana_Project_URL__c` = `https://app.asana.com/0/{!AsanaProjectGID}/list`
|
||||||
|
|
||||||
|
**See:** [Integration: Salesforce → Asana](../../integrations/salesforce-to-asana.md) for detailed API specs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.4: Create Asana Sections & Tasks
|
||||||
|
|
||||||
|
**Element Type:** Loop + HTTP Callouts
|
||||||
|
|
||||||
|
**For Each Section:** (Discovery, Architecture, Build, Testing, Go-Live)
|
||||||
|
|
||||||
|
1. **Create Section:**
|
||||||
|
```
|
||||||
|
POST /projects/{!AsanaProjectGID}/sections
|
||||||
|
{"data": {"name": "Discovery"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Tasks in Section:**
|
||||||
|
|
||||||
|
**Discovery Section:**
|
||||||
|
- Task: "Confirm domain + DNS with client" → Assign to Sales Ops
|
||||||
|
- Task: "Gather branding assets" → Assign to Design team
|
||||||
|
|
||||||
|
**Architecture Section:**
|
||||||
|
- Task: "Wire up Railway/Cloudflare environments" → Assign to DevOps
|
||||||
|
- Task: "Enable CI/CD secrets" → Assign to DevOps
|
||||||
|
- Task: "Design database schema" → Assign to Backend team
|
||||||
|
|
||||||
|
**Build Section:**
|
||||||
|
- Task: "Set up database and migrations" → Assign to Backend
|
||||||
|
- Task: "Implement authentication" → Assign to Backend
|
||||||
|
- Task: "Build core UI components" → Assign to Frontend
|
||||||
|
|
||||||
|
**Testing Section:**
|
||||||
|
- Task: "Run end-to-end test suite" → Assign to QA
|
||||||
|
- Task: "Security scan and review" → Assign to Security team
|
||||||
|
|
||||||
|
**Go-Live Section:**
|
||||||
|
- Task: "Final client walkthrough" → Assign to Customer Success
|
||||||
|
- Task: "Deploy to production" → Assign to DevOps
|
||||||
|
|
||||||
|
**Each Task Payload:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"name": "{!TaskName}",
|
||||||
|
"projects": ["{!AsanaProjectGID}"],
|
||||||
|
"memberships": [{"project": "{!AsanaProjectGID}", "section": "{!SectionGID}"}],
|
||||||
|
"assignee": "{!AssigneeEmail}",
|
||||||
|
"due_on": "{!CalculatedDueDate}",
|
||||||
|
"notes": "Salesforce Project: {!Project.Id}\nGitHub Repos: {!Project.Backend_Repo_URL__c}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.5: Wait for Repositories to Be Verified
|
||||||
|
|
||||||
|
**Element Type:** Interactive Step - Approval
|
||||||
|
|
||||||
|
**Approver:** DevOps Manager
|
||||||
|
**Approval Criteria:**
|
||||||
|
- [ ] All 3 repos created and accessible
|
||||||
|
- [ ] CI/CD workflows active
|
||||||
|
- [ ] Branch protection enabled
|
||||||
|
- [ ] Secrets configured
|
||||||
|
|
||||||
|
**Timeout:** 2 days
|
||||||
|
**Escalation:** If not approved in 2 days, send alert to Engineering Manager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2.6: Update Project Status
|
||||||
|
|
||||||
|
**Element Type:** Action - Update Records
|
||||||
|
|
||||||
|
**Record:** Project__c
|
||||||
|
**Fields:**
|
||||||
|
- `Status__c` = "Customer Onboarding"
|
||||||
|
- `Technical_Setup_Completed__c` = `TODAY()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3: Customer Onboarding
|
||||||
|
|
||||||
|
**Owner:** Customer Success
|
||||||
|
**Duration:** 5-10 days
|
||||||
|
**Status Field:** `Project__c.Status__c` = "Customer Onboarding"
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 3.1: Schedule Kickoff Call
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Event
|
||||||
|
|
||||||
|
**Assigned To:** Customer Success Manager
|
||||||
|
**Invitees:**
|
||||||
|
- Primary Contact (from Opportunity)
|
||||||
|
- Technical Owner (from Project)
|
||||||
|
- Account Executive
|
||||||
|
|
||||||
|
**Subject:** `Kickoff Call: {!Project.Name}`
|
||||||
|
**Duration:** 1 hour
|
||||||
|
**Due Date:** `TODAY() + 3`
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
```
|
||||||
|
Agenda:
|
||||||
|
1. Introductions (5 min)
|
||||||
|
2. Project overview and timeline (10 min)
|
||||||
|
3. Requirements review (20 min)
|
||||||
|
4. Q&A (20 min)
|
||||||
|
5. Next steps (5 min)
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Review Opportunity notes
|
||||||
|
- Review Asana project board
|
||||||
|
- Prepare questions about domain, branding, integrations
|
||||||
|
|
||||||
|
Links:
|
||||||
|
- Salesforce Project: {!Project.Id}
|
||||||
|
- Asana Board: {!Project.Asana_Project_URL__c}
|
||||||
|
- GitHub Repos: {!Project.Backend_Repo_URL__c}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3.2: Wait for Development Milestones
|
||||||
|
|
||||||
|
**Element Type:** Wait - Condition-Based
|
||||||
|
|
||||||
|
**Condition:**
|
||||||
|
- At least 50% of Asana tasks in "Build" section marked complete
|
||||||
|
**OR**
|
||||||
|
- Project status manually changed
|
||||||
|
**OR**
|
||||||
|
- 10 days elapsed
|
||||||
|
|
||||||
|
**Check Frequency:** Daily
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3.3: Send Progress Update to Client
|
||||||
|
|
||||||
|
**Element Type:** Action - Send Email (every week)
|
||||||
|
|
||||||
|
**Template:** `Client_Progress_Update`
|
||||||
|
|
||||||
|
**To:** Primary Contact
|
||||||
|
**CC:** Account Executive, Technical Owner
|
||||||
|
|
||||||
|
**Merge Fields:**
|
||||||
|
- Progress summary from Asana
|
||||||
|
- Link to staging environment (if available)
|
||||||
|
- Next milestones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3.4: Wait for Testing Complete
|
||||||
|
|
||||||
|
**Element Type:** Interactive Step - Checkbox
|
||||||
|
|
||||||
|
**Screen:** "Testing Complete?"
|
||||||
|
|
||||||
|
**Assigned To:** QA Lead
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- [ ] All end-to-end tests pass
|
||||||
|
- [ ] Security scan clean
|
||||||
|
- [ ] Performance acceptable
|
||||||
|
- [ ] Client UAT completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3.5: Update Project Status
|
||||||
|
|
||||||
|
**Element Type:** Action - Update Records
|
||||||
|
|
||||||
|
**Record:** Project__c
|
||||||
|
**Fields:**
|
||||||
|
- `Status__c` = "Review & Go-Live"
|
||||||
|
- `Onboarding_Completed__c` = `TODAY()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4: Review & Go-Live
|
||||||
|
|
||||||
|
**Owner:** Customer Success + DevOps
|
||||||
|
**Duration:** 2-3 days
|
||||||
|
**Status Field:** `Project__c.Status__c` = "Review & Go-Live"
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 4.1: Final Client Walkthrough
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Event
|
||||||
|
|
||||||
|
**Assigned To:** Customer Success Manager
|
||||||
|
**Invitees:** Primary Contact, Technical Owner
|
||||||
|
|
||||||
|
**Subject:** `Final Review: {!Project.Name}`
|
||||||
|
**Duration:** 30 minutes
|
||||||
|
**Due Date:** `TODAY() + 1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.2: Wait for Client Approval
|
||||||
|
|
||||||
|
**Element Type:** Interactive Step - Screen Flow
|
||||||
|
|
||||||
|
**Screen:** "Client Approval"
|
||||||
|
|
||||||
|
**Assigned To:** Customer Success Manager
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- Radio: "Client approval status" (Approved / Needs Changes / Rejected)
|
||||||
|
- Text Area: "Client feedback"
|
||||||
|
- Checkbox: "Client trained on system"
|
||||||
|
|
||||||
|
**Conditional Logic:**
|
||||||
|
- If "Needs Changes" → Loop back to Stage 3, Step 3.2
|
||||||
|
- If "Rejected" → Escalate to Sales leadership
|
||||||
|
- If "Approved" → Continue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.3: Production Deployment
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Task
|
||||||
|
|
||||||
|
**Assigned To:** DevOps team
|
||||||
|
**Subject:** `Deploy to Production: {!Project.Project_Key__c}`
|
||||||
|
**Priority:** High
|
||||||
|
**Due Date:** `TODAY() + 1`
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
```
|
||||||
|
Project is client-approved and ready for production deployment.
|
||||||
|
|
||||||
|
Action:
|
||||||
|
1. Verify staging is stable
|
||||||
|
2. Create production Railway/Cloudflare environments
|
||||||
|
3. Merge to main and deploy (will trigger GitHub Actions)
|
||||||
|
4. Verify health checks
|
||||||
|
5. Mark this task complete
|
||||||
|
|
||||||
|
Links:
|
||||||
|
- Asana Deploy Task: {!Project.Asana_Project_URL__c}
|
||||||
|
- Backend Repo: {!Project.Backend_Repo_URL__c}
|
||||||
|
- Ops Repo: {!Project.Ops_Repo_URL__c}
|
||||||
|
|
||||||
|
Note: Deployment will auto-update Salesforce Project record when complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.4: Wait for Deploy Task Complete
|
||||||
|
|
||||||
|
**Element Type:** Wait - Task Completion
|
||||||
|
|
||||||
|
**Wait For:** Task created in Step 4.3 to be marked complete
|
||||||
|
|
||||||
|
**Timeout:** 3 days
|
||||||
|
**Escalation:** Alert Engineering Manager if not completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.5: Send Go-Live Notification
|
||||||
|
|
||||||
|
**Element Type:** Action - Send Email
|
||||||
|
|
||||||
|
**Template:** `Client_GoLive_Notification`
|
||||||
|
|
||||||
|
**To:** Primary Contact
|
||||||
|
**CC:** Account Executive, Customer Success Manager
|
||||||
|
|
||||||
|
**Merge Fields:**
|
||||||
|
- Production URL: `{!Project.Frontend_URL__c}`
|
||||||
|
- Support contact info
|
||||||
|
- Documentation links
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.6: Update Project Status to Active
|
||||||
|
|
||||||
|
**Element Type:** Action - Update Records
|
||||||
|
|
||||||
|
**Record:** Project__c
|
||||||
|
**Fields:**
|
||||||
|
- `Status__c` = "Active"
|
||||||
|
- `Go_Live_Date__c` = `TODAY()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4.7: Create Success Metrics Dashboard
|
||||||
|
|
||||||
|
**Element Type:** Action - Create Dashboard (optional)
|
||||||
|
|
||||||
|
**Purpose:** Track ongoing project health
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Deploy frequency
|
||||||
|
- Error rates
|
||||||
|
- Customer support tickets
|
||||||
|
- Feature adoption
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Orchestration Complete
|
||||||
|
|
||||||
|
**Final Actions:**
|
||||||
|
|
||||||
|
1. **Post to Chatter:** Announce successful onboarding
|
||||||
|
2. **Update Opportunity:** Set `Stage` = "Closed Won - Active"
|
||||||
|
3. **Create Renewal Opportunity:** Schedule for renewal date
|
||||||
|
4. **Archive Orchestration:** Store for audit trail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### If GitHub API Fails (Step 2.1-2.2)
|
||||||
|
|
||||||
|
1. Log error to Salesforce
|
||||||
|
2. Create high-priority task for DevOps
|
||||||
|
3. Send alert to #ops Slack
|
||||||
|
4. Pause orchestration (manual resume after fix)
|
||||||
|
|
||||||
|
### If Asana API Fails (Step 2.3-2.4)
|
||||||
|
|
||||||
|
1. Log error
|
||||||
|
2. Continue orchestration (Asana is nice-to-have)
|
||||||
|
3. Create manual Asana project (via SOP)
|
||||||
|
|
||||||
|
### If Client Approval Rejected (Step 4.2)
|
||||||
|
|
||||||
|
1. Loop back to appropriate stage based on feedback
|
||||||
|
2. Create issue in GitHub with client feedback
|
||||||
|
3. Notify engineering team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Metrics
|
||||||
|
|
||||||
|
### Track These KPIs:
|
||||||
|
|
||||||
|
| Metric | Target | Measured By |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Time to First Commit | < 3 days from Closed Won | GitHub first commit timestamp |
|
||||||
|
| Time to Go-Live | < 20 days from Closed Won | `Go_Live_Date__c` - `Start_Date__c` |
|
||||||
|
| Orchestration Completion Rate | > 95% | Successful completions / total starts |
|
||||||
|
| Stage Escalations | < 5% | Escalations triggered / total projects |
|
||||||
|
| Client Approval First-Time Rate | > 80% | Approved without loops / total |
|
||||||
|
|
||||||
|
### Weekly Review:
|
||||||
|
- How many orchestrations in flight?
|
||||||
|
- Any stuck in a stage for > expected duration?
|
||||||
|
- Any escalations or errors?
|
||||||
|
|
||||||
|
### Monthly Audit:
|
||||||
|
- Review all completed orchestrations
|
||||||
|
- Identify bottlenecks
|
||||||
|
- Update stage durations based on actuals
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Instructions
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
1. Create all custom objects and fields (see Flow spec)
|
||||||
|
2. Set up Named Credentials (GitHub, Asana, Slack)
|
||||||
|
3. Create email templates
|
||||||
|
4. Set up approval processes
|
||||||
|
|
||||||
|
### Build Orchestration:
|
||||||
|
|
||||||
|
1. **Setup → Orchestrator → New Orchestration**
|
||||||
|
2. Name: `New_Client_Kickoff_Orchestration`
|
||||||
|
3. Add stages (1-4) as specified
|
||||||
|
4. For each stage, add steps as elements
|
||||||
|
5. Configure inputs/outputs
|
||||||
|
6. Set wait conditions
|
||||||
|
7. Add error handlers
|
||||||
|
8. Activate
|
||||||
|
|
||||||
|
### Test in Sandbox:
|
||||||
|
|
||||||
|
1. Create test opportunity
|
||||||
|
2. Move to Closed Won
|
||||||
|
3. Verify orchestration starts
|
||||||
|
4. Walk through each stage
|
||||||
|
5. Verify APIs called correctly
|
||||||
|
6. Check error handling
|
||||||
|
|
||||||
|
### Deploy to Production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sf project deploy start \
|
||||||
|
--source-dir force-app/main/default/orchestrations/New_Client_Kickoff_Orchestration.orchestration-meta.xml \
|
||||||
|
--target-org production
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Salesforce Flow: Opp_Automation_OnStageChange](../flows/opp-automation-onstagechange.md)
|
||||||
|
- [Workflow: New Client Kickoff](../../workflows/new-client-kickoff.md)
|
||||||
|
- [Integration: Salesforce → GitHub](../../integrations/salesforce-to-github.md)
|
||||||
|
- [Integration: Salesforce → Asana](../../integrations/salesforce-to-asana.md)
|
||||||
|
- [Playbook: Brenda's New Client Checklist](../../playbooks/brenda-new-client-checklist.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Date | Version | Change | Author |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| 2025-11-17 | 1.0 | Initial specification | Cece (Claude) |
|
||||||
189
sop/templates/github-actions/ci.yml
Normal file
189
sop/templates/github-actions/ci.yml
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
name: CI Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test (Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11", "3.12"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pytest pytest-cov pytest-asyncio pytest-mock
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: |
|
||||||
|
pytest --cov=. --cov-report=xml --cov-report=term-missing --cov-report=html -v
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
if: matrix.python-version == '3.12'
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
- name: Archive coverage report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: matrix.python-version == '3.12'
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: htmlcov/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint & Format Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install linting tools
|
||||||
|
run: |
|
||||||
|
pip install ruff black isort mypy
|
||||||
|
|
||||||
|
- name: Run ruff (fast Python linter)
|
||||||
|
run: |
|
||||||
|
ruff check . --output-format=github
|
||||||
|
|
||||||
|
- name: Check code formatting with black
|
||||||
|
run: |
|
||||||
|
black --check --diff .
|
||||||
|
|
||||||
|
- name: Check import sorting with isort
|
||||||
|
run: |
|
||||||
|
isort --check-only --diff .
|
||||||
|
|
||||||
|
- name: Type check with mypy
|
||||||
|
run: |
|
||||||
|
mypy . --ignore-missing-imports --show-error-codes
|
||||||
|
continue-on-error: true # Don't fail CI on type errors initially
|
||||||
|
|
||||||
|
security:
|
||||||
|
name: Security Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install safety & bandit
|
||||||
|
run: |
|
||||||
|
pip install safety bandit[toml]
|
||||||
|
|
||||||
|
- name: Check for known vulnerabilities in dependencies
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt
|
||||||
|
safety check --json || true
|
||||||
|
|
||||||
|
- name: Run bandit security linter
|
||||||
|
run: |
|
||||||
|
bandit -r . -f json -o bandit-report.json || true
|
||||||
|
bandit -r . -f screen
|
||||||
|
|
||||||
|
- name: Upload security reports
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: security-reports
|
||||||
|
path: |
|
||||||
|
bandit-report.json
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build & Test Container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
tags: test-build:${{ github.sha }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
load: true
|
||||||
|
|
||||||
|
- name: Test container starts and responds
|
||||||
|
run: |
|
||||||
|
docker run -d --name test-container -p 8000:8000 test-build:${{ github.sha }}
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check if container is still running
|
||||||
|
if [ "$(docker inspect -f '{{.State.Running}}' test-container)" != "true" ]; then
|
||||||
|
echo "Container failed to start"
|
||||||
|
docker logs test-container
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check health endpoint
|
||||||
|
curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
|
docker stop test-container
|
||||||
|
docker rm test-container
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: CI Summary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [test, lint, security, build]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check all jobs status
|
||||||
|
run: |
|
||||||
|
echo "Test: ${{ needs.test.result }}"
|
||||||
|
echo "Lint: ${{ needs.lint.result }}"
|
||||||
|
echo "Security: ${{ needs.security.result }}"
|
||||||
|
echo "Build: ${{ needs.build.result }}"
|
||||||
|
|
||||||
|
if [ "${{ needs.test.result }}" != "success" ] || \
|
||||||
|
[ "${{ needs.lint.result }}" != "success" ] || \
|
||||||
|
[ "${{ needs.build.result }}" != "success" ]; then
|
||||||
|
echo "❌ CI pipeline failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All CI checks passed"
|
||||||
303
sop/templates/github-actions/deploy.yml
Normal file
303
sop/templates/github-actions/deploy.yml
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
default: 'production'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
|
||||||
|
env:
|
||||||
|
RAILWAY_SERVICE: backend
|
||||||
|
ENVIRONMENT: ${{ github.event.inputs.environment || 'production' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-backend:
|
||||||
|
name: Deploy Backend to Railway
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ github.event.inputs.environment || 'production' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Railway CLI
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://railway.app/install.sh | sh
|
||||||
|
echo "$HOME/.railway/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Deploy to Railway
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
railway up --service ${{ env.RAILWAY_SERVICE }} --environment ${{ env.ENVIRONMENT }}
|
||||||
|
|
||||||
|
- name: Wait for deployment to complete
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "Waiting for Railway deployment to stabilize..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
- name: Get deployment URL
|
||||||
|
id: get-url
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
DEPLOY_URL=$(railway domain --service ${{ env.RAILWAY_SERVICE }} --environment ${{ env.ENVIRONMENT }} 2>/dev/null || echo "")
|
||||||
|
echo "DEPLOY_URL=$DEPLOY_URL" >> $GITHUB_OUTPUT
|
||||||
|
echo "Backend deployed to: $DEPLOY_URL"
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
if: steps.get-url.outputs.DEPLOY_URL != ''
|
||||||
|
run: |
|
||||||
|
echo "Running health check on ${{ steps.get-url.outputs.DEPLOY_URL }}"
|
||||||
|
|
||||||
|
# Retry health check up to 10 times
|
||||||
|
for i in {1..10}; do
|
||||||
|
if curl -f "${{ steps.get-url.outputs.DEPLOY_URL }}/health" 2>/dev/null; then
|
||||||
|
echo "✅ Health check passed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Health check attempt $i failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ Health check failed after 10 attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Save deployment metadata
|
||||||
|
id: metadata
|
||||||
|
run: |
|
||||||
|
echo "DEPLOY_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
|
||||||
|
echo "DEPLOY_SHA=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "DEPLOY_ACTOR=${{ github.actor }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "DEPLOY_BRANCH=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
deploy_url: ${{ steps.get-url.outputs.DEPLOY_URL }}
|
||||||
|
deploy_time: ${{ steps.metadata.outputs.DEPLOY_TIME }}
|
||||||
|
deploy_sha: ${{ steps.metadata.outputs.DEPLOY_SHA }}
|
||||||
|
|
||||||
|
deploy-frontend:
|
||||||
|
name: Deploy Frontend to Cloudflare Pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ github.event.inputs.environment || 'production' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
env:
|
||||||
|
VITE_API_URL: ${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_ENV: ${{ env.ENVIRONMENT }}
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/pages-action@v1
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||||
|
directory: dist
|
||||||
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Verify frontend deployment
|
||||||
|
run: |
|
||||||
|
FRONTEND_URL="${{ secrets.CLOUDFLARE_PAGES_URL }}"
|
||||||
|
echo "Verifying frontend at $FRONTEND_URL"
|
||||||
|
|
||||||
|
# Wait for Cloudflare propagation
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Check if frontend loads
|
||||||
|
for i in {1..5}; do
|
||||||
|
if curl -f "$FRONTEND_URL" 2>/dev/null; then
|
||||||
|
echo "✅ Frontend is accessible"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ Frontend verification failed"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
notify-stakeholders:
|
||||||
|
name: Notify Stakeholders
|
||||||
|
needs: [deploy-backend, deploy-frontend]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: success()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Extract Project Key from repo name
|
||||||
|
id: project-key
|
||||||
|
run: |
|
||||||
|
# Extract from repo name: blackroad-ACME-X7K9-backend → ACME-X7K9
|
||||||
|
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 from repo name"
|
||||||
|
PROJECT_KEY="UNKNOWN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PROJECT_KEY=$PROJECT_KEY" >> $GITHUB_OUTPUT
|
||||||
|
echo "Extracted project key: $PROJECT_KEY"
|
||||||
|
|
||||||
|
- name: Update Salesforce Project record
|
||||||
|
if: steps.project-key.outputs.PROJECT_KEY != 'UNKNOWN'
|
||||||
|
env:
|
||||||
|
SF_INSTANCE_URL: ${{ secrets.SALESFORCE_INSTANCE_URL }}
|
||||||
|
SF_ACCESS_TOKEN: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
curl -X PATCH \
|
||||||
|
"$SF_INSTANCE_URL/services/data/v58.0/sobjects/Project__c/Project_Key__c/${{ steps.project-key.outputs.PROJECT_KEY }}" \
|
||||||
|
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"Last_Deploy_At__c": "${{ needs.deploy-backend.outputs.deploy_time }}",
|
||||||
|
"Last_Deploy_SHA__c": "${{ needs.deploy-backend.outputs.deploy_sha }}",
|
||||||
|
"Last_Deploy_Branch__c": "${{ github.ref_name }}",
|
||||||
|
"Last_Deploy_Actor__c": "${{ github.actor }}",
|
||||||
|
"Deploy_Status__c": "Success",
|
||||||
|
"Environment__c": "${{ env.ENVIRONMENT }}",
|
||||||
|
"Backend_URL__c": "${{ needs.deploy-backend.outputs.deploy_url }}",
|
||||||
|
"Release_Notes_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||||
|
}' || echo "Warning: Failed to update Salesforce"
|
||||||
|
|
||||||
|
- name: Create Salesforce Deployment record
|
||||||
|
if: steps.project-key.outputs.PROJECT_KEY != 'UNKNOWN'
|
||||||
|
env:
|
||||||
|
SF_INSTANCE_URL: ${{ secrets.SALESFORCE_INSTANCE_URL }}
|
||||||
|
SF_ACCESS_TOKEN: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
|
||||||
|
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": "${{ steps.project-key.outputs.PROJECT_KEY }} - ${{ github.sha }}",
|
||||||
|
"Project_Key__c": "${{ steps.project-key.outputs.PROJECT_KEY }}",
|
||||||
|
"Deployed_At__c": "${{ needs.deploy-backend.outputs.deploy_time }}",
|
||||||
|
"Git_SHA__c": "${{ github.sha }}",
|
||||||
|
"Git_Branch__c": "${{ github.ref_name }}",
|
||||||
|
"Deployed_By__c": "${{ github.actor }}",
|
||||||
|
"Status__c": "Success",
|
||||||
|
"Environment__c": "${{ env.ENVIRONMENT }}",
|
||||||
|
"Repository__c": "${{ github.repository }}",
|
||||||
|
"Commit_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||||
|
}' || echo "Warning: Failed to create Deployment record"
|
||||||
|
|
||||||
|
- name: Update Asana tasks
|
||||||
|
if: steps.project-key.outputs.PROJECT_KEY != 'UNKNOWN' && env.ENVIRONMENT == 'production'
|
||||||
|
env:
|
||||||
|
ASANA_PAT: ${{ secrets.ASANA_PAT }}
|
||||||
|
ASANA_WORKSPACE_GID: ${{ secrets.ASANA_WORKSPACE_GID }}
|
||||||
|
run: |
|
||||||
|
PROJECT_KEY="${{ steps.project-key.outputs.PROJECT_KEY }}"
|
||||||
|
|
||||||
|
# Find Asana project by name containing PROJECT_KEY
|
||||||
|
PROJECT_GID=$(curl -s "https://app.asana.com/api/1.0/projects?workspace=$ASANA_WORKSPACE_GID&opt_fields=name,gid" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" | \
|
||||||
|
jq -r ".data[] | select(.name | contains(\"$PROJECT_KEY\")) | .gid" | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$PROJECT_GID" ]; then
|
||||||
|
echo "Warning: Could not find Asana project for $PROJECT_KEY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found Asana project: $PROJECT_GID"
|
||||||
|
|
||||||
|
# Find "Deploy" task
|
||||||
|
TASK_GID=$(curl -s "https://app.asana.com/api/1.0/projects/$PROJECT_GID/tasks?opt_fields=name,gid,completed" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" | \
|
||||||
|
jq -r '.data[] | select(.name | test("Deploy.*production"; "i")) | select(.completed == false) | .gid' | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$TASK_GID" ]; then
|
||||||
|
echo "No pending deploy task found in Asana"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found deploy task: $TASK_GID"
|
||||||
|
|
||||||
|
# Mark task as complete
|
||||||
|
curl -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"data": {"completed": true}}' || echo "Warning: Failed to complete task"
|
||||||
|
|
||||||
|
# Add comment with deploy details
|
||||||
|
curl -X POST "https://app.asana.com/api/1.0/tasks/$TASK_GID/stories" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"text": "✅ Deployed to production\n\n**Commit:** ${{ github.sha }}\n**By:** ${{ github.actor }}\n**Time:** ${{ needs.deploy-backend.outputs.deploy_time }}\n**Link:** https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||||
|
}
|
||||||
|
}' || echo "Warning: Failed to add comment"
|
||||||
|
|
||||||
|
- name: Notify Slack
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEPLOYS }}
|
||||||
|
run: |
|
||||||
|
STATUS_EMOJI="✅"
|
||||||
|
curl -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "'"$STATUS_EMOJI"' *Deploy to '"${{ env.ENVIRONMENT }}"'*\n\n*Project:* '"${{ steps.project-key.outputs.PROJECT_KEY }}"'\n*Repo:* `${{ github.repository }}`\n*Commit:* <https://github.com/${{ github.repository }}/commit/${{ github.sha }}|'"${GITHUB_SHA:0:7}"'>\n*By:* ${{ github.actor }}\n*Backend:* ${{ needs.deploy-backend.outputs.deploy_url }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}' || echo "Warning: Failed to send Slack notification"
|
||||||
|
|
||||||
|
rollback-on-failure:
|
||||||
|
name: Rollback on Failure
|
||||||
|
needs: [deploy-backend, deploy-frontend]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: failure()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify failure
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEPLOYS }}
|
||||||
|
run: |
|
||||||
|
curl -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "❌ *Deploy FAILED*\n\n*Repo:* `${{ github.repository }}`\n*Commit:* <https://github.com/${{ github.repository }}/commit/${{ github.sha }}|'"${GITHUB_SHA:0:7}"'>\n*By:* ${{ github.actor }}\n\n⚠️ Manual intervention may be required. Check the <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|workflow run> for details."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}' || true
|
||||||
238
sop/templates/github-actions/safety.yml
Normal file
238
sop/templates/github-actions/safety.yml
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
name: Security & Safety Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Monday at 9am UTC
|
||||||
|
- cron: '0 9 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-scan:
|
||||||
|
name: Dependency Vulnerability Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install safety pip-audit
|
||||||
|
|
||||||
|
- name: Run safety check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
safety check --json --output safety-report.json || true
|
||||||
|
safety check
|
||||||
|
|
||||||
|
- name: Run pip-audit
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
pip-audit --format json --output pip-audit-report.json || true
|
||||||
|
pip-audit
|
||||||
|
|
||||||
|
- name: Upload vulnerability reports
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dependency-vulnerability-reports
|
||||||
|
path: |
|
||||||
|
safety-report.json
|
||||||
|
pip-audit-report.json
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
sast-scan:
|
||||||
|
name: Static Application Security Testing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install bandit
|
||||||
|
run: pip install bandit[toml]
|
||||||
|
|
||||||
|
- name: Run bandit security linter
|
||||||
|
run: |
|
||||||
|
bandit -r . \
|
||||||
|
-f json \
|
||||||
|
-o bandit-report.json \
|
||||||
|
--exclude './tests/*,./venv/*,./.venv/*,*/site-packages/*' \
|
||||||
|
-ll || true
|
||||||
|
|
||||||
|
bandit -r . \
|
||||||
|
-f screen \
|
||||||
|
--exclude './tests/*,./venv/*,./.venv/*,*/site-packages/*' \
|
||||||
|
-ll
|
||||||
|
|
||||||
|
- name: Upload SAST report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: sast-report
|
||||||
|
path: bandit-report.json
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
secret-scan:
|
||||||
|
name: Secret Detection
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Full history for secret scanning
|
||||||
|
|
||||||
|
- name: Run Gitleaks
|
||||||
|
uses: gitleaks/gitleaks-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}}
|
||||||
|
|
||||||
|
codeql:
|
||||||
|
name: CodeQL Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: python, javascript
|
||||||
|
queries: security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: "/language:python"
|
||||||
|
|
||||||
|
license-check:
|
||||||
|
name: License Compliance Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install pip-licenses
|
||||||
|
run: pip install pip-licenses
|
||||||
|
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Check licenses
|
||||||
|
run: |
|
||||||
|
echo "Generating license report..."
|
||||||
|
pip-licenses --format=json --output-file=licenses.json
|
||||||
|
pip-licenses --format=markdown --output-file=licenses.md
|
||||||
|
|
||||||
|
# Check for problematic licenses (GPL, AGPL, etc.)
|
||||||
|
PROBLEMATIC=$(pip-licenses --format=json | jq -r '.[] | select(.License | test("GPL|AGPL")) | .Name')
|
||||||
|
|
||||||
|
if [ -n "$PROBLEMATIC" ]; then
|
||||||
|
echo "⚠️ Warning: Found packages with potentially problematic licenses:"
|
||||||
|
echo "$PROBLEMATIC"
|
||||||
|
# Don't fail the build, just warn
|
||||||
|
else
|
||||||
|
echo "✅ No problematic licenses found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload license report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: license-report
|
||||||
|
path: |
|
||||||
|
licenses.json
|
||||||
|
licenses.md
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
container-scan:
|
||||||
|
name: Container Image Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name != 'schedule'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t security-scan:${{ github.sha }} .
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: security-scan:${{ github.sha }}
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
|
||||||
|
- name: Upload Trivy results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
|
||||||
|
- name: Run Trivy for readable output
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: security-scan:${{ github.sha }}
|
||||||
|
format: 'table'
|
||||||
|
severity: 'CRITICAL,HIGH,MEDIUM'
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: Security Summary
|
||||||
|
needs: [dependency-scan, sast-scan, secret-scan, license-check]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Security check summary
|
||||||
|
run: |
|
||||||
|
echo "### Security Scan Results"
|
||||||
|
echo ""
|
||||||
|
echo "- Dependency Scan: ${{ needs.dependency-scan.result }}"
|
||||||
|
echo "- SAST Scan: ${{ needs.sast-scan.result }}"
|
||||||
|
echo "- Secret Scan: ${{ needs.secret-scan.result }}"
|
||||||
|
echo "- License Check: ${{ needs.license-check.result }}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "${{ needs.secret-scan.result }}" == "failure" ]; then
|
||||||
|
echo "❌ CRITICAL: Secrets detected in repository!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.dependency-scan.result }}" == "failure" ] || \
|
||||||
|
[ "${{ needs.sast-scan.result }}" == "failure" ]; then
|
||||||
|
echo "⚠️ Security vulnerabilities detected. Review the reports."
|
||||||
|
# Don't fail the build for vulnerability warnings
|
||||||
|
else
|
||||||
|
echo "✅ No critical security issues detected"
|
||||||
|
fi
|
||||||
61
sop/templates/repo-template/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
61
sop/templates/repo-template/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or unexpected behavior
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'type:bug, priority:p2'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- Clear and concise description of the bug -->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!-- What should happen -->
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
|
||||||
|
<!-- What actually happens -->
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- **Environment:** (production / staging / local)
|
||||||
|
- **Browser/Client:** (if applicable)
|
||||||
|
- **OS:** (if applicable)
|
||||||
|
- **Version/Commit:**
|
||||||
|
|
||||||
|
## Screenshots/Logs
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots or relevant error logs -->
|
||||||
|
|
||||||
|
```
|
||||||
|
Paste error logs here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Possible Fix
|
||||||
|
|
||||||
|
<!-- Optional: Suggest a potential solution or workaround -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Any other relevant information -->
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- [ ] Blocks production users
|
||||||
|
- [ ] Blocks internal users
|
||||||
|
- [ ] Minor inconvenience
|
||||||
|
- [ ] Nice to have fix
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Asana Task:** [link]
|
||||||
|
- **Salesforce Case:** [link]
|
||||||
|
- **Related Issues:** #
|
||||||
114
sop/templates/repo-template/.github/ISSUE_TEMPLATE/deployment_checklist.md
vendored
Normal file
114
sop/templates/repo-template/.github/ISSUE_TEMPLATE/deployment_checklist.md
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
name: Deployment Checklist
|
||||||
|
about: Pre/post deployment checklist
|
||||||
|
title: '[DEPLOY] Release v'
|
||||||
|
labels: 'type:deployment, priority:p1'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Information
|
||||||
|
|
||||||
|
- **Version:** v
|
||||||
|
- **Target Date:**
|
||||||
|
- **Environment:** (staging / production)
|
||||||
|
- **Deployment Window:**
|
||||||
|
|
||||||
|
## Pre-Deployment Checklist
|
||||||
|
|
||||||
|
### Code & Testing
|
||||||
|
- [ ] All PRs merged to `main`
|
||||||
|
- [ ] CI pipeline passes (tests, lint, build)
|
||||||
|
- [ ] Security scans pass
|
||||||
|
- [ ] Code review completed
|
||||||
|
- [ ] Release notes drafted
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- [ ] Database migrations tested in staging
|
||||||
|
- [ ] Environment variables updated (if needed)
|
||||||
|
- [ ] Secrets rotated (if needed)
|
||||||
|
- [ ] Resource scaling planned (if needed)
|
||||||
|
|
||||||
|
### Stakeholder Communication
|
||||||
|
- [ ] Asana tasks updated with deploy plan
|
||||||
|
- [ ] Salesforce Project record current
|
||||||
|
- [ ] #deploys channel notified
|
||||||
|
- [ ] Customer success team informed (if customer-facing changes)
|
||||||
|
|
||||||
|
### Backup & Rollback
|
||||||
|
- [ ] Database backup created
|
||||||
|
- [ ] Current version tagged for rollback
|
||||||
|
- [ ] Rollback procedure documented
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
1. [ ] Tag release in GitHub
|
||||||
|
2. [ ] Trigger deploy workflow
|
||||||
|
3. [ ] Monitor deployment logs
|
||||||
|
4. [ ] Wait for health checks to pass
|
||||||
|
5. [ ] Verify backend API responding
|
||||||
|
6. [ ] Verify frontend loads correctly
|
||||||
|
|
||||||
|
## Post-Deployment Checklist
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- [ ] Health checks passing
|
||||||
|
- [ ] Critical user flows tested
|
||||||
|
- [ ] Database migrations applied
|
||||||
|
- [ ] No errors in application logs
|
||||||
|
- [ ] No spike in error rates (monitoring)
|
||||||
|
- [ ] Performance metrics within acceptable range
|
||||||
|
|
||||||
|
### Stakeholder Updates
|
||||||
|
- [ ] Salesforce Project record updated (automated)
|
||||||
|
- [ ] Asana deploy task marked complete (automated)
|
||||||
|
- [ ] Slack notification sent (automated)
|
||||||
|
- [ ] Release notes published
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- [ ] Set up alerts for next 24 hours
|
||||||
|
- [ ] Monitor error rates
|
||||||
|
- [ ] Monitor performance metrics
|
||||||
|
- [ ] Check user feedback channels
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
**Trigger Rollback If:**
|
||||||
|
- Critical functionality broken
|
||||||
|
- Error rate > 5%
|
||||||
|
- Performance degradation > 50%
|
||||||
|
- Database corruption detected
|
||||||
|
|
||||||
|
**Rollback Procedure:**
|
||||||
|
1. Navigate to: https://github.com/$REPO/actions/workflows/rollback.yml
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Enter previous stable SHA:
|
||||||
|
4. Enter rollback reason:
|
||||||
|
5. Monitor rollback completion
|
||||||
|
|
||||||
|
## Issues Discovered
|
||||||
|
|
||||||
|
<!-- Document any issues found during/after deployment -->
|
||||||
|
|
||||||
|
| Issue | Severity | Status | Resolution |
|
||||||
|
|-------|----------|--------|------------|
|
||||||
|
| | | | |
|
||||||
|
|
||||||
|
## Post-Mortem Notes
|
||||||
|
|
||||||
|
<!-- After deployment, document lessons learned -->
|
||||||
|
|
||||||
|
**What went well:**
|
||||||
|
-
|
||||||
|
|
||||||
|
**What could be improved:**
|
||||||
|
-
|
||||||
|
|
||||||
|
**Action items:**
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Release Notes:** [link]
|
||||||
|
- **Salesforce Project:** [link]
|
||||||
|
- **Asana Deploy Task:** [link]
|
||||||
57
sop/templates/repo-template/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
57
sop/templates/repo-template/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'type:feature, priority:p3'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem/Need
|
||||||
|
|
||||||
|
<!-- What problem does this feature solve? Who needs it? -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!-- Describe your proposed solution -->
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
<!-- What other approaches did you consider? Why is this the best option? -->
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
<!-- How will users interact with this feature? -->
|
||||||
|
|
||||||
|
As a **[user type]**, I want **[goal]** so that **[benefit]**.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
<!-- How will we know this feature is successful? -->
|
||||||
|
|
||||||
|
- [ ] Criterion 1
|
||||||
|
- [ ] Criterion 2
|
||||||
|
- [ ] Criterion 3
|
||||||
|
|
||||||
|
## Technical Considerations
|
||||||
|
|
||||||
|
<!-- Any technical constraints, dependencies, or implementation notes -->
|
||||||
|
|
||||||
|
## Mockups/Designs
|
||||||
|
|
||||||
|
<!-- Optional: Add mockups, wireframes, or design references -->
|
||||||
|
|
||||||
|
## Priority Justification
|
||||||
|
|
||||||
|
- **Business Value:** (High / Medium / Low)
|
||||||
|
- **Technical Complexity:** (High / Medium / Low)
|
||||||
|
- **User Impact:** (High / Medium / Low)
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Asana Epic:** [link]
|
||||||
|
- **Salesforce Opportunity:** [link]
|
||||||
|
- **Related Issues:** #
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Any other relevant information -->
|
||||||
206
sop/templates/repo-template/.github/branch-protection.md
vendored
Normal file
206
sop/templates/repo-template/.github/branch-protection.md
vendored
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Branch Protection Configuration
|
||||||
|
|
||||||
|
This document specifies the branch protection rules to be applied to all BlackRoad project repositories.
|
||||||
|
|
||||||
|
## Main Branch Protection
|
||||||
|
|
||||||
|
**Branch:** `main`
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
**Require Pull Request:**
|
||||||
|
- ✅ Require a pull request before merging
|
||||||
|
- Require approvals: **1**
|
||||||
|
- Dismiss stale pull request approvals when new commits are pushed
|
||||||
|
- Require review from Code Owners (if CODEOWNERS file exists)
|
||||||
|
|
||||||
|
**Status Checks:**
|
||||||
|
- ✅ Require status checks to pass before merging
|
||||||
|
- ✅ Require branches to be up to date before merging
|
||||||
|
|
||||||
|
**Required Status Checks:**
|
||||||
|
- `test (3.11)`
|
||||||
|
- `test (3.12)`
|
||||||
|
- `lint`
|
||||||
|
- `build`
|
||||||
|
- `security / summary` (allow to fail)
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- ✅ Restrict who can push to matching branches
|
||||||
|
- Allowed to push: **Repository admins only**
|
||||||
|
- Allowed to bypass: **None** (not even admins)
|
||||||
|
|
||||||
|
**Other Rules:**
|
||||||
|
- ✅ Require linear history (enforce rebase or squash merge)
|
||||||
|
- ✅ Require deployments to succeed before merging (if applicable)
|
||||||
|
- ✅ Lock branch (prevent all changes) - **❌ Disabled** (allow normal development)
|
||||||
|
- ✅ Do not allow force pushes
|
||||||
|
- ✅ Do not allow deletions
|
||||||
|
|
||||||
|
**Enforcement:**
|
||||||
|
- ✅ Include administrators (admins must follow the same rules)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Branch Protection (Optional)
|
||||||
|
|
||||||
|
**Branch:** `develop` (if using GitFlow)
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
**Require Pull Request:**
|
||||||
|
- ✅ Require a pull request before merging
|
||||||
|
- Require approvals: **1**
|
||||||
|
|
||||||
|
**Status Checks:**
|
||||||
|
- ✅ Require status checks to pass before merging
|
||||||
|
- Required checks: `test`, `lint`, `build`
|
||||||
|
|
||||||
|
**Other Rules:**
|
||||||
|
- ✅ Do not allow force pushes
|
||||||
|
- ✅ Do not allow deletions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag Protection
|
||||||
|
|
||||||
|
**Pattern:** `v*` (all version tags)
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
- ✅ Only repository admins can create tags matching this pattern
|
||||||
|
- ✅ Only repository admins can delete tags matching this pattern
|
||||||
|
|
||||||
|
**Purpose:** Prevent accidental or malicious deletion of release tags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Via GitHub API
|
||||||
|
|
||||||
|
Use this script to apply branch protection rules programmatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
REPO="blackboxprogramming/blackroad-{PROJECT_KEY}-backend"
|
||||||
|
BRANCH="main"
|
||||||
|
TOKEN="${GITHUB_TOKEN}"
|
||||||
|
|
||||||
|
curl -X PUT \
|
||||||
|
"https://api.github.com/repos/${REPO}/branches/${BRANCH}/protection" \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
-d '{
|
||||||
|
"required_status_checks": {
|
||||||
|
"strict": true,
|
||||||
|
"contexts": [
|
||||||
|
"test (3.11)",
|
||||||
|
"test (3.12)",
|
||||||
|
"lint",
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enforce_admins": true,
|
||||||
|
"required_pull_request_reviews": {
|
||||||
|
"dismissal_restrictions": {},
|
||||||
|
"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
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via GitHub Web UI
|
||||||
|
|
||||||
|
1. Go to repository → Settings → Branches
|
||||||
|
2. Click "Add branch protection rule"
|
||||||
|
3. Branch name pattern: `main`
|
||||||
|
4. Configure settings as specified above
|
||||||
|
5. Click "Create" or "Save changes"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CODEOWNERS File
|
||||||
|
|
||||||
|
Create `.github/CODEOWNERS` to automatically request reviews from specific teams:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Default owners for everything
|
||||||
|
* @blackboxprogramming/engineering
|
||||||
|
|
||||||
|
# Backend code
|
||||||
|
/backend/ @blackboxprogramming/backend-team
|
||||||
|
|
||||||
|
# Frontend code
|
||||||
|
/frontend/ @blackboxprogramming/frontend-team
|
||||||
|
|
||||||
|
# Infrastructure
|
||||||
|
/ops/ @blackboxprogramming/devops-team
|
||||||
|
/terraform/ @blackboxprogramming/devops-team
|
||||||
|
/.github/workflows/ @blackboxprogramming/devops-team
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
/docs/ @blackboxprogramming/documentation-team
|
||||||
|
*.md @blackboxprogramming/documentation-team
|
||||||
|
|
||||||
|
# Security-sensitive files
|
||||||
|
/secrets/ @blackboxprogramming/security-team
|
||||||
|
.env.* @blackboxprogramming/security-team
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Merge Strategy
|
||||||
|
|
||||||
|
**Preferred:** Squash and merge
|
||||||
|
|
||||||
|
**Reasoning:**
|
||||||
|
- Clean, linear history
|
||||||
|
- Each PR becomes a single commit
|
||||||
|
- Easy to revert if needed
|
||||||
|
- Clear attribution
|
||||||
|
|
||||||
|
**Alternative:** Rebase and merge (for repos with well-structured commit history)
|
||||||
|
|
||||||
|
**Avoid:** Merge commits (creates messy history)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
**When to bypass branch protection:**
|
||||||
|
- **NEVER** for regular development
|
||||||
|
- Only in absolute emergencies:
|
||||||
|
- Critical production bug fix (with manager approval)
|
||||||
|
- Security vulnerability patch (with security team approval)
|
||||||
|
- Service outage (with on-call engineer approval)
|
||||||
|
|
||||||
|
**Process for emergency bypass:**
|
||||||
|
1. Get approval in #ops or #engineering Slack channel
|
||||||
|
2. Document reason in channel
|
||||||
|
3. Make the emergency change
|
||||||
|
4. Create follow-up PR immediately after to document the change
|
||||||
|
5. Post-mortem within 24 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
**Weekly:** Review bypass logs
|
||||||
|
**Monthly:** Audit branch protection settings across all repos
|
||||||
|
**Quarterly:** Review and update required status checks
|
||||||
|
|
||||||
|
**Tool:** Use GitHub audit log API to track who bypassed protection and why
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [GitHub Actions: CI Workflow](../../github-actions/ci.yml)
|
||||||
|
- [New Client Kickoff Workflow](../../workflows/new-client-kickoff.md)
|
||||||
|
- [Pull Request Template](./pull_request_template.md)
|
||||||
137
sop/templates/repo-template/.github/labels.json
vendored
Normal file
137
sop/templates/repo-template/.github/labels.json
vendored
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "type:feature",
|
||||||
|
"color": "0E8A16",
|
||||||
|
"description": "New feature or enhancement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:bug",
|
||||||
|
"color": "D73A4A",
|
||||||
|
"description": "Bug or defect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:docs",
|
||||||
|
"color": "0075CA",
|
||||||
|
"description": "Documentation changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:refactor",
|
||||||
|
"color": "FBCA04",
|
||||||
|
"description": "Code refactoring without functionality change"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:test",
|
||||||
|
"color": "BFD4F2",
|
||||||
|
"description": "Test-related changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:chore",
|
||||||
|
"color": "FEF2C0",
|
||||||
|
"description": "Maintenance, dependencies, tooling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:security",
|
||||||
|
"color": "D93F0B",
|
||||||
|
"description": "Security-related changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type:deployment",
|
||||||
|
"color": "5319E7",
|
||||||
|
"description": "Deployment or release-related"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "priority:p0",
|
||||||
|
"color": "B60205",
|
||||||
|
"description": "Critical - Drop everything"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "priority:p1",
|
||||||
|
"color": "D93F0B",
|
||||||
|
"description": "High - Should be next"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "priority:p2",
|
||||||
|
"color": "FBCA04",
|
||||||
|
"description": "Medium - Normal priority"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "priority:p3",
|
||||||
|
"color": "C5DEF5",
|
||||||
|
"description": "Low - Nice to have"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "area:backend",
|
||||||
|
"color": "5319E7",
|
||||||
|
"description": "Backend/API changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "area:frontend",
|
||||||
|
"color": "1D76DB",
|
||||||
|
"description": "Frontend/UI changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "area:ops",
|
||||||
|
"color": "0E8A16",
|
||||||
|
"description": "Infrastructure/DevOps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "area:infra",
|
||||||
|
"color": "006B75",
|
||||||
|
"description": "Infrastructure as code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "area:database",
|
||||||
|
"color": "5319E7",
|
||||||
|
"description": "Database/schema changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status:blocked",
|
||||||
|
"color": "D93F0B",
|
||||||
|
"description": "Blocked by external dependency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status:in-review",
|
||||||
|
"color": "FBCA04",
|
||||||
|
"description": "In code review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status:needs-info",
|
||||||
|
"color": "D876E3",
|
||||||
|
"description": "Needs more information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status:wontfix",
|
||||||
|
"color": "FFFFFF",
|
||||||
|
"description": "Will not be addressed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"color": "0E8A16",
|
||||||
|
"description": "Trigger deployment on merge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "breaking-change",
|
||||||
|
"color": "D93F0B",
|
||||||
|
"description": "Contains breaking changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "good-first-issue",
|
||||||
|
"color": "7057FF",
|
||||||
|
"description": "Good for newcomers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "help-wanted",
|
||||||
|
"color": "008672",
|
||||||
|
"description": "Extra attention needed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "automation",
|
||||||
|
"color": "BFDADC",
|
||||||
|
"description": "Automation or workflow changes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "automation-bug",
|
||||||
|
"color": "D93F0B",
|
||||||
|
"description": "Automation system malfunction"
|
||||||
|
}
|
||||||
|
]
|
||||||
50
sop/templates/repo-template/.github/pull_request_template.md
vendored
Normal file
50
sop/templates/repo-template/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## What
|
||||||
|
|
||||||
|
<!-- Brief description of what this PR changes -->
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
<!-- Business/technical justification for these changes -->
|
||||||
|
|
||||||
|
## How
|
||||||
|
|
||||||
|
<!-- Implementation approach and key technical decisions -->
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [ ] Unit tests added/updated
|
||||||
|
- [ ] Integration tests pass
|
||||||
|
- [ ] Manual testing completed
|
||||||
|
- [ ] Edge cases considered
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Code follows project style guidelines
|
||||||
|
- [ ] Self-reviewed the code
|
||||||
|
- [ ] Commented complex/non-obvious code
|
||||||
|
- [ ] Documentation updated (if applicable)
|
||||||
|
- [ ] No new warnings introduced
|
||||||
|
- [ ] Backward compatibility maintained (or breaking changes documented)
|
||||||
|
|
||||||
|
## Screenshots/Logs
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots or relevant logs -->
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Asana Task:** [link]
|
||||||
|
- **Salesforce Project:** [link]
|
||||||
|
- **Related Issues:** Closes #
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
<!-- Any special deployment steps, migrations, or configuration changes needed -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Reviewer Checklist:**
|
||||||
|
- [ ] Code is clean and maintainable
|
||||||
|
- [ ] Tests are adequate
|
||||||
|
- [ ] No security vulnerabilities introduced
|
||||||
|
- [ ] Performance implications considered
|
||||||
|
- [ ] Documentation is clear
|
||||||
384
sop/workflows/new-client-kickoff.md
Normal file
384
sop/workflows/new-client-kickoff.md
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
# New Client / Project Kickoff Workflow
|
||||||
|
|
||||||
|
**Owner:** Operations
|
||||||
|
**Systems:** Salesforce → GitHub → Asana
|
||||||
|
**Status:** Active
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is the **flagship automation workflow** that orchestrates the entire journey from "deal closed" to "project ready for development." It connects Salesforce, GitHub, and Asana to eliminate manual setup and ensure consistency.
|
||||||
|
|
||||||
|
**Flow Duration:** ~5-10 minutes (automated)
|
||||||
|
**Human Touch Points:** Initial validation + final review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Golden Path
|
||||||
|
|
||||||
|
```
|
||||||
|
Salesforce Opp (Closed Won)
|
||||||
|
↓
|
||||||
|
Salesforce Flow creates Project record
|
||||||
|
↓
|
||||||
|
Orchestration kicks off (4 stages)
|
||||||
|
↓
|
||||||
|
Stage 2: Technical Setup
|
||||||
|
↓
|
||||||
|
├─→ GitHub: Create repos + CI/CD
|
||||||
|
└─→ Asana: Create project + tasks
|
||||||
|
↓
|
||||||
|
Feedback loops: Deploy events → Salesforce + Asana
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage-by-Stage Breakdown
|
||||||
|
|
||||||
|
### **Stage A: Salesforce – Opportunity Closure**
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
Sales Stage moves to `Closed Won` on Opportunity
|
||||||
|
|
||||||
|
**Salesforce Flow:** `Opp_Automation_OnStageChange`
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
|
||||||
|
1. **Validate required fields:**
|
||||||
|
- Account Name
|
||||||
|
- Primary Contact
|
||||||
|
- Domain/Subdomain
|
||||||
|
- Package Type (OS / Console / Custom)
|
||||||
|
- Service Tier (Starter / Pro / Enterprise)
|
||||||
|
|
||||||
|
2. **Create/Update Records:**
|
||||||
|
- Link or create Account record
|
||||||
|
- Link or create Contact record (Primary)
|
||||||
|
- Create **Project** record (Custom Object: `Project__c`)
|
||||||
|
|
||||||
|
3. **Project Record Fields:**
|
||||||
|
```
|
||||||
|
Project_Key__c: {ACCOUNT_SLUG}-{RANDOM_4CHAR}
|
||||||
|
Example: ACME-X7K9
|
||||||
|
|
||||||
|
Account__c: Lookup to Account
|
||||||
|
Opportunity__c: Lookup to Opportunity
|
||||||
|
Primary_Domain__c: Text (e.g., "acme-portal")
|
||||||
|
Package_Type__c: Picklist (OS / Console / Custom)
|
||||||
|
Service_Tier__c: Picklist (Starter / Pro / Enterprise)
|
||||||
|
Start_Date__c: Date (auto: today)
|
||||||
|
Status__c: Picklist → "Setup In Progress"
|
||||||
|
Technical_Owner__c: Lookup to User
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Launch Orchestration:**
|
||||||
|
- Name: `New_Client_Kickoff_Orchestration`
|
||||||
|
- Input: Project record ID
|
||||||
|
- Stages:
|
||||||
|
- Sales Handoff
|
||||||
|
- Technical Setup ← **We care about this**
|
||||||
|
- Customer Onboarding
|
||||||
|
- Review & Go-Live
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
✅ Project record created
|
||||||
|
✅ Orchestration started
|
||||||
|
✅ Notification sent to #ops Slack channel (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Stage B: GitHub – Repository Scaffolding**
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
Orchestration Stage "Technical Setup" begins
|
||||||
|
|
||||||
|
**Mechanism:**
|
||||||
|
Salesforce **Autolaunched Flow** → HTTP Callout to GitHub API (or GitHub App webhook receiver)
|
||||||
|
|
||||||
|
**API Endpoint:**
|
||||||
|
```
|
||||||
|
POST https://api.github.com/orgs/blackboxprogramming/repos
|
||||||
|
Authorization: Bearer {GITHUB_APP_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payload (per repo):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "blackroad-{project_key}-backend",
|
||||||
|
"description": "Backend for {Account Name} ({Project Key})",
|
||||||
|
"private": true,
|
||||||
|
"auto_init": true,
|
||||||
|
"gitignore_template": "Python",
|
||||||
|
"license_template": "mit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Repos Created:**
|
||||||
|
|
||||||
|
1. `blackroad-{project_key}-backend`
|
||||||
|
2. `blackroad-{project_key}-frontend`
|
||||||
|
3. `blackroad-{project_key}-ops`
|
||||||
|
|
||||||
|
**Apply Repo Template** (via GitHub API or Actions):
|
||||||
|
|
||||||
|
1. **Branch Protection** (on `main`):
|
||||||
|
- Require PR before merge
|
||||||
|
- Require 1 approval
|
||||||
|
- Require status checks: `test`, `lint`, `build`
|
||||||
|
- No force pushes
|
||||||
|
- No deletions
|
||||||
|
|
||||||
|
2. **Labels** (via API):
|
||||||
|
```
|
||||||
|
type:feature, type:bug, type:docs, type:refactor
|
||||||
|
priority:p0, priority:p1, priority:p2, priority:p3
|
||||||
|
area:backend, area:frontend, area:ops, area:infra
|
||||||
|
status:blocked, status:in-review
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Issue Templates** (from `sop/templates/repo-template/.github/ISSUE_TEMPLATE/`):
|
||||||
|
- `bug_report.md`
|
||||||
|
- `feature_request.md`
|
||||||
|
- `deployment_checklist.md`
|
||||||
|
|
||||||
|
4. **PR Template** (`.github/pull_request_template.md`)
|
||||||
|
|
||||||
|
5. **GitHub Actions Workflows** (from `sop/templates/github-actions/`):
|
||||||
|
- `.github/workflows/ci.yml` – Run tests + lint on every push/PR
|
||||||
|
- `.github/workflows/deploy.yml` – Deploy on release tag or approved PR to `main`
|
||||||
|
- `.github/workflows/safety.yml` – SAST + dependency scanning
|
||||||
|
- `.github/workflows/notify-salesforce.yml` – Send deploy events back to Salesforce
|
||||||
|
|
||||||
|
**Secrets Setup** (manual or via API):
|
||||||
|
```
|
||||||
|
RAILWAY_TOKEN
|
||||||
|
CLOUDFLARE_API_TOKEN
|
||||||
|
SALESFORCE_WEBHOOK_URL
|
||||||
|
ASANA_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
✅ 3 repos created
|
||||||
|
✅ CI/CD pipelines active
|
||||||
|
✅ Branch protection enabled
|
||||||
|
✅ Repo URLs written back to Salesforce Project record:
|
||||||
|
- `Backend_Repo_URL__c`
|
||||||
|
- `Frontend_Repo_URL__c`
|
||||||
|
- `Ops_Repo_URL__c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Stage C: Asana – Task Space for Humans**
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
Same Orchestration Stage "Technical Setup", after GitHub repos created
|
||||||
|
|
||||||
|
**Mechanism:**
|
||||||
|
Salesforce Flow → HTTP Callout to Asana API (or via Zapier/Make)
|
||||||
|
|
||||||
|
**API Endpoint:**
|
||||||
|
```
|
||||||
|
POST https://app.asana.com/api/1.0/projects
|
||||||
|
Authorization: Bearer {ASANA_PAT}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payload:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"name": "{Account Name} - {Project Key}",
|
||||||
|
"notes": "Salesforce Project: {Project_URL}\nRepos:\n- Backend: {Backend_Repo_URL}\n- Frontend: {Frontend_Repo_URL}\n- Ops: {Ops_Repo_URL}",
|
||||||
|
"workspace": "{WORKSPACE_GID}",
|
||||||
|
"team": "{TEAM_GID}",
|
||||||
|
"default_view": "board",
|
||||||
|
"color": "light-green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Sections:**
|
||||||
|
```
|
||||||
|
POST /projects/{project_gid}/sections
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"name": "Discovery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat for: `Architecture`, `Build`, `Testing`, `Go-Live`
|
||||||
|
|
||||||
|
**Create Standard Tasks:**
|
||||||
|
|
||||||
|
| Section | Task | Assignee | Description |
|
||||||
|
|---------|------|----------|-------------|
|
||||||
|
| Discovery | Confirm domain + DNS with client | Sales Ops | Get final domain, subdomain, and DNS setup requirements |
|
||||||
|
| Discovery | Gather branding assets | Design | Logo, colors, fonts for custom theming |
|
||||||
|
| Architecture | Wire up Railway/Cloudflare envs | DevOps | Create staging + production environments |
|
||||||
|
| Architecture | Enable CI/CD secrets | DevOps | Add RAILWAY_TOKEN, CLOUDFLARE_API_TOKEN to GitHub |
|
||||||
|
| Build | Set up database schema | Backend | Initialize Postgres + migrations |
|
||||||
|
| Build | Implement authentication | Backend | SSO or email/password setup |
|
||||||
|
| Testing | Run first end-to-end test | QA | Verify deploy pipeline works end-to-end |
|
||||||
|
| Go-Live | Final client walkthrough | Customer Success | Demo + training session |
|
||||||
|
|
||||||
|
**Each Task Contains:**
|
||||||
|
- Link to Salesforce Project record
|
||||||
|
- Link to relevant GitHub repo + issues
|
||||||
|
- Due date (calculated from Project start date + offsets)
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
✅ Asana project created
|
||||||
|
✅ Standard task set populated
|
||||||
|
✅ Asana project URL written back to Salesforce:
|
||||||
|
- `Asana_Project_URL__c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Stage D: Feedback Loop – GitHub → Salesforce & Asana**
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
GitHub events:
|
||||||
|
- PR merged to `main` with label `release`
|
||||||
|
- GitHub Release created
|
||||||
|
- CI pipeline `deploy` job succeeds
|
||||||
|
|
||||||
|
**Mechanism:**
|
||||||
|
GitHub Action: `.github/workflows/notify-salesforce.yml`
|
||||||
|
|
||||||
|
**Action Workflow (Pseudocode):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Notify Salesforce & Asana on Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Deploy"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Extract deploy metadata
|
||||||
|
run: |
|
||||||
|
echo "SHA=${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
echo "REPO=${{ github.repository }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Update Salesforce Project
|
||||||
|
run: |
|
||||||
|
curl -X PATCH "$SALESFORCE_WEBHOOK_URL/services/data/v58.0/sobjects/Project__c/ExternalId__c/${{ secrets.PROJECT_KEY }}" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.SALESFORCE_ACCESS_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"Last_Deploy_At__c": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
|
||||||
|
"Last_Deploy_SHA__c": "'$SHA'",
|
||||||
|
"Last_Deploy_Tag__c": "'$TAG'",
|
||||||
|
"Environment__c": "production",
|
||||||
|
"Release_Notes_URL__c": "https://github.com/'$REPO'/releases/tag/'$TAG'"
|
||||||
|
}'
|
||||||
|
|
||||||
|
- name: Complete Asana deploy task
|
||||||
|
run: |
|
||||||
|
TASK_GID=$(curl -s "https://app.asana.com/api/1.0/projects/${{ secrets.ASANA_PROJECT_GID }}/tasks?opt_fields=name,gid" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.ASANA_PAT }}" | \
|
||||||
|
jq -r '.data[] | select(.name | contains("Deploy to production")) | .gid')
|
||||||
|
|
||||||
|
curl -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.ASANA_PAT }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"data": {"completed": true}}'
|
||||||
|
|
||||||
|
# Post comment
|
||||||
|
curl -X POST "https://app.asana.com/api/1.0/tasks/$TASK_GID/stories" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.ASANA_PAT }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"data": {"text": "✅ Deployed '$TAG' from '$REPO'@'$SHA' to production"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
✅ Salesforce Project updated with latest deploy info
|
||||||
|
✅ Asana tasks auto-completed
|
||||||
|
✅ Asana comments added with deploy details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Human Touch Points (Brenda View)
|
||||||
|
|
||||||
|
**What You See:**
|
||||||
|
|
||||||
|
1. **Day 0 – Deal Closes:**
|
||||||
|
- You mark Opportunity as "Closed Won" in Salesforce
|
||||||
|
- Wait 5-10 minutes ⏱️
|
||||||
|
|
||||||
|
2. **Day 0 – Automatic Magic:**
|
||||||
|
- Repos appear in GitHub (you get a Slack notification)
|
||||||
|
- Asana project appears with all tasks assigned
|
||||||
|
- No manual work needed
|
||||||
|
|
||||||
|
3. **Throughout Project:**
|
||||||
|
- Engineers merge code → tasks auto-update in Asana
|
||||||
|
- Deploy happens → Salesforce shows "Last Deploy: 2 hours ago"
|
||||||
|
- You just monitor Asana and communicate with client
|
||||||
|
|
||||||
|
**If Something Breaks:**
|
||||||
|
- Create an issue in `blackroad-sop` repo with label `automation-bug`
|
||||||
|
- Tag @ops or @devops team
|
||||||
|
- Include: Salesforce Project URL, expected vs. actual behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
|
||||||
|
After this workflow runs, verify:
|
||||||
|
|
||||||
|
- [ ] Project record exists in Salesforce with all fields populated
|
||||||
|
- [ ] 3 repos created in GitHub with correct naming
|
||||||
|
- [ ] CI/CD workflows present in each repo
|
||||||
|
- [ ] Branch protection enabled on `main`
|
||||||
|
- [ ] Asana project created with 8+ tasks
|
||||||
|
- [ ] Salesforce Project has Asana + GitHub URLs filled in
|
||||||
|
- [ ] Test deploy updates Salesforce + Asana correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely Cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| No repos created after 10 min | GitHub API token expired | Refresh token in Salesforce Named Credential |
|
||||||
|
| Asana project missing | Webhook failed | Check Salesforce debug logs for HTTP callout errors |
|
||||||
|
| Deploy doesn't update Salesforce | Missing secret `PROJECT_KEY` | Add `PROJECT_KEY` secret to each repo |
|
||||||
|
| Tasks not auto-completing | Asana API token wrong | Verify `ASANA_PAT` secret in GitHub |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Docs
|
||||||
|
|
||||||
|
- [Salesforce Flow Spec: Opp_Automation_OnStageChange](../salesforce/flows/opp-automation-onstagechange.md)
|
||||||
|
- [Salesforce Orchestration Spec](../salesforce/orchestrations/new-client-kickoff-orchestration.md)
|
||||||
|
- [GitHub Actions: CI Baseline](../templates/github-actions/ci.yml)
|
||||||
|
- [Integration: Salesforce → GitHub](../integrations/salesforce-to-github.md)
|
||||||
|
- [Integration: GitHub → Salesforce](../integrations/github-to-salesforce.md)
|
||||||
|
- [Playbook: Brenda's New Client Checklist](../playbooks/brenda-new-client-checklist.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metrics to Track
|
||||||
|
|
||||||
|
- **Time to First Commit:** From Closed Won to first commit in project repo (target: < 48 hours)
|
||||||
|
- **Automation Success Rate:** % of deals that auto-create repos + Asana (target: > 95%)
|
||||||
|
- **Manual Intervention Rate:** % of projects requiring manual fixes (target: < 10%)
|
||||||
|
- **Deploy Frequency:** Avg deploys per week per project (target: > 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**This is the backbone.** Every other automation workflow should follow this pattern:
|
||||||
|
1. Event-driven trigger
|
||||||
|
2. Cross-system orchestration
|
||||||
|
3. Feedback loops
|
||||||
|
4. Human-friendly visibility
|
||||||
556
sop/workflows/release-pipeline.md
Normal file
556
sop/workflows/release-pipeline.md
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
# Release Pipeline Workflow
|
||||||
|
|
||||||
|
**Owner:** Engineering + DevOps
|
||||||
|
**Systems:** GitHub → Railway/Cloudflare → Salesforce → Asana
|
||||||
|
**Status:** Active
|
||||||
|
**Last Updated:** 2025-11-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This workflow automates the **entire release and deployment lifecycle**, from code commit to production deployment to updating all stakeholders automatically.
|
||||||
|
|
||||||
|
**Key Principle:** Merge to `main` should feel like magic. Everything else happens automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Developer pushes code
|
||||||
|
↓
|
||||||
|
GitHub Actions: CI pipeline (test + lint + build)
|
||||||
|
↓
|
||||||
|
PASS → Ready for PR
|
||||||
|
↓
|
||||||
|
PR created + reviewed + approved
|
||||||
|
↓
|
||||||
|
PR merged to `main`
|
||||||
|
↓
|
||||||
|
GitHub Actions: Deploy pipeline
|
||||||
|
↓
|
||||||
|
├─→ Deploy to Railway (backend)
|
||||||
|
├─→ Deploy to Cloudflare Pages (frontend)
|
||||||
|
└─→ Update configs (ops)
|
||||||
|
↓
|
||||||
|
Deploy succeeds
|
||||||
|
↓
|
||||||
|
GitHub Actions: Notify stakeholders
|
||||||
|
↓
|
||||||
|
├─→ Update Salesforce Project (deploy metadata)
|
||||||
|
├─→ Complete Asana tasks (deployment checklist)
|
||||||
|
└─→ Post to Slack #deploys
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1: Continuous Integration (CI)
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
- Every push to any branch
|
||||||
|
- Every pull request opened/updated
|
||||||
|
|
||||||
|
**GitHub Action:** `.github/workflows/ci.yml`
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
|
||||||
|
### 1.1 Test
|
||||||
|
```yaml
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.11, 3.12]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pytest pytest-cov
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest --cov=. --cov-report=xml
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
if: matrix.python-version == '3.12'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Lint
|
||||||
|
```yaml
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
- name: Install linters
|
||||||
|
run: pip install ruff black isort mypy
|
||||||
|
- name: Run ruff
|
||||||
|
run: ruff check .
|
||||||
|
- name: Check formatting
|
||||||
|
run: black --check .
|
||||||
|
- name: Check import order
|
||||||
|
run: isort --check-only .
|
||||||
|
- name: Type check
|
||||||
|
run: mypy . --ignore-missing-imports
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Build
|
||||||
|
```yaml
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t test-build .
|
||||||
|
- name: Test container starts
|
||||||
|
run: |
|
||||||
|
docker run -d --name test-container test-build
|
||||||
|
sleep 5
|
||||||
|
docker logs test-container
|
||||||
|
docker stop test-container
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
✅ All checks pass → PR can be merged
|
||||||
|
❌ Any check fails → PR blocked until fixed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2: Pull Request Flow
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
|
||||||
|
1. **Developer creates PR** with standardized template:
|
||||||
|
```markdown
|
||||||
|
## What
|
||||||
|
Brief description of changes
|
||||||
|
|
||||||
|
## Why
|
||||||
|
Business/technical justification
|
||||||
|
|
||||||
|
## How
|
||||||
|
Implementation approach
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- [ ] Unit tests added/updated
|
||||||
|
- [ ] Integration tests pass
|
||||||
|
- [ ] Manual testing completed
|
||||||
|
|
||||||
|
## Screenshots/Logs
|
||||||
|
(if applicable)
|
||||||
|
|
||||||
|
## Related
|
||||||
|
- Asana Task: [link]
|
||||||
|
- Salesforce Project: [link]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Automated checks run:**
|
||||||
|
- CI pipeline (test + lint + build)
|
||||||
|
- CODEOWNERS review assignment
|
||||||
|
- Label auto-applied based on files changed
|
||||||
|
|
||||||
|
3. **Human review:**
|
||||||
|
- At least 1 approval required
|
||||||
|
- No unresolved conversations
|
||||||
|
- All checks green
|
||||||
|
|
||||||
|
4. **Merge:**
|
||||||
|
- Squash and merge (clean history)
|
||||||
|
- Auto-delete branch after merge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3: Continuous Deployment (CD)
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
- Push to `main` branch
|
||||||
|
- GitHub Release created (for versioned deploys)
|
||||||
|
|
||||||
|
**GitHub Action:** `.github/workflows/deploy.yml`
|
||||||
|
|
||||||
|
### 3.1 Deploy Backend (Railway)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Railway CLI
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://railway.app/install.sh | sh
|
||||||
|
|
||||||
|
- name: Deploy to Railway
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
railway up --service backend --environment production
|
||||||
|
|
||||||
|
- name: Wait for deployment
|
||||||
|
run: |
|
||||||
|
railway status --service backend --environment production --wait
|
||||||
|
|
||||||
|
- name: Run post-deploy health check
|
||||||
|
run: |
|
||||||
|
BACKEND_URL=$(railway variables get BACKEND_URL --service backend --environment production)
|
||||||
|
curl -f "$BACKEND_URL/health" || exit 1
|
||||||
|
|
||||||
|
- name: Save deploy metadata
|
||||||
|
run: |
|
||||||
|
echo "DEPLOY_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_SHA=${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Deploy Frontend (Cloudflare Pages)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
env:
|
||||||
|
VITE_API_URL: ${{ secrets.PROD_API_URL }}
|
||||||
|
VITE_ENV: production
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/pages-action@v1
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
projectName: blackroad-frontend
|
||||||
|
directory: dist
|
||||||
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Verify deployment
|
||||||
|
run: |
|
||||||
|
sleep 30 # Wait for propagation
|
||||||
|
curl -f "https://blackroad.app" || exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Update Infrastructure (if ops repo)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
update-infra:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(github.repository, '-ops')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Apply Terraform changes
|
||||||
|
env:
|
||||||
|
TF_VAR_railway_token: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
TF_VAR_cloudflare_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cd terraform
|
||||||
|
terraform init
|
||||||
|
terraform plan -out=tfplan
|
||||||
|
terraform apply -auto-approve tfplan
|
||||||
|
|
||||||
|
- name: Update Cloudflare DNS
|
||||||
|
if: contains(github.event.head_commit.message, '[dns]')
|
||||||
|
run: |
|
||||||
|
# Apply DNS changes from dns-config.json
|
||||||
|
python scripts/update_cloudflare_dns.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4: Stakeholder Notification
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
Deploy jobs complete successfully
|
||||||
|
|
||||||
|
**GitHub Action:** `.github/workflows/notify-stakeholders.yml`
|
||||||
|
|
||||||
|
### 4.1 Update Salesforce
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
update-salesforce:
|
||||||
|
needs: [deploy-backend, deploy-frontend]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get Project Key from repo
|
||||||
|
run: |
|
||||||
|
# Extract from repo name: blackroad-ACME-X7K9-backend → ACME-X7K9
|
||||||
|
REPO_NAME="${{ github.repository }}"
|
||||||
|
PROJECT_KEY=$(echo "$REPO_NAME" | sed -n 's/.*blackroad-\([A-Z0-9-]*\)-.*/\1/p')
|
||||||
|
echo "PROJECT_KEY=$PROJECT_KEY" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Update Salesforce Project record
|
||||||
|
env:
|
||||||
|
SF_INSTANCE_URL: ${{ secrets.SALESFORCE_INSTANCE_URL }}
|
||||||
|
SF_ACCESS_TOKEN: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
|
||||||
|
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 }}"
|
||||||
|
}'
|
||||||
|
|
||||||
|
- 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 '{
|
||||||
|
"Project__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",
|
||||||
|
"Repository__c": "${{ github.repository }}",
|
||||||
|
"Commit_URL__c": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Update Asana
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
update-asana:
|
||||||
|
needs: [deploy-backend, deploy-frontend]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get Asana project and task IDs
|
||||||
|
env:
|
||||||
|
ASANA_PAT: ${{ secrets.ASANA_PAT }}
|
||||||
|
PROJECT_KEY: ${{ env.PROJECT_KEY }}
|
||||||
|
run: |
|
||||||
|
# Find project by name containing PROJECT_KEY
|
||||||
|
PROJECT_GID=$(curl -s "https://app.asana.com/api/1.0/projects?workspace=${{ secrets.ASANA_WORKSPACE_GID }}&opt_fields=name,gid" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" | \
|
||||||
|
jq -r '.data[] | select(.name | contains("'$PROJECT_KEY'")) | .gid')
|
||||||
|
|
||||||
|
echo "ASANA_PROJECT_GID=$PROJECT_GID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Find and complete deploy task
|
||||||
|
run: |
|
||||||
|
# Find "Deploy to production" task
|
||||||
|
TASK_GID=$(curl -s "https://app.asana.com/api/1.0/projects/$ASANA_PROJECT_GID/tasks?opt_fields=name,gid,completed" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" | \
|
||||||
|
jq -r '.data[] | select(.name | contains("Deploy") and (.completed == false)) | .gid' | head -n 1)
|
||||||
|
|
||||||
|
if [ -n "$TASK_GID" ]; then
|
||||||
|
# Mark as complete
|
||||||
|
curl -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"data": {"completed": true}}'
|
||||||
|
|
||||||
|
# Add comment with deploy details
|
||||||
|
curl -X POST "https://app.asana.com/api/1.0/tasks/$TASK_GID/stories" \
|
||||||
|
-H "Authorization: Bearer $ASANA_PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"text": "✅ Deployed to production\n\n**Commit:** ${{ github.sha }}\n**By:** ${{ github.actor }}\n**Time:** '"$(date -u)"'\n**Link:** https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Notify Slack
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notify-slack:
|
||||||
|
needs: [update-salesforce, update-asana]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Post to #deploys channel
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEPLOYS }}
|
||||||
|
run: |
|
||||||
|
STATUS_EMOJI="${{ job.status == 'success' && '✅' || '❌' }}"
|
||||||
|
curl -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "'"$STATUS_EMOJI"' *Deploy to Production*\n\n*Project:* '"$PROJECT_KEY"'\n*Repo:* `${{ github.repository }}`\n*Commit:* <https://github.com/${{ github.repository }}/commit/${{ github.sha }}|'"${GITHUB_SHA:0:7}"'>\n*By:* ${{ github.actor }}\n*Status:* ${{ job.status }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 5: Rollback (if needed)
|
||||||
|
|
||||||
|
**Trigger:** Manual action or automated health check failure
|
||||||
|
|
||||||
|
**GitHub Action:** `.github/workflows/rollback.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Rollback Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target_sha:
|
||||||
|
description: 'Git SHA to rollback to'
|
||||||
|
required: true
|
||||||
|
reason:
|
||||||
|
description: 'Reason for rollback'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rollback:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.target_sha }}
|
||||||
|
|
||||||
|
- name: Rollback backend
|
||||||
|
run: |
|
||||||
|
railway up --service backend --environment production
|
||||||
|
|
||||||
|
- name: Rollback frontend
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
# Deploy to Cloudflare...
|
||||||
|
|
||||||
|
- name: Update Salesforce
|
||||||
|
run: |
|
||||||
|
curl -X POST "$SF_INSTANCE_URL/services/data/v58.0/sobjects/Deployment__c" \
|
||||||
|
-H "Authorization: Bearer $SF_ACCESS_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"Project__c": "'"$PROJECT_KEY"'",
|
||||||
|
"Status__c": "Rollback",
|
||||||
|
"Git_SHA__c": "${{ github.event.inputs.target_sha }}",
|
||||||
|
"Rollback_Reason__c": "${{ github.event.inputs.reason }}"
|
||||||
|
}'
|
||||||
|
|
||||||
|
- name: Notify team
|
||||||
|
run: |
|
||||||
|
curl -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-d '{
|
||||||
|
"text": "⚠️ *ROLLBACK PERFORMED*\n\nProject: '"$PROJECT_KEY"'\nRolled back to: '"${{ github.event.inputs.target_sha }}"'\nReason: '"${{ github.event.inputs.reason }}"'\nBy: '"${{ github.actor }}"'"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Human Touch Points
|
||||||
|
|
||||||
|
**What Developers Do:**
|
||||||
|
|
||||||
|
1. Write code in feature branch
|
||||||
|
2. Create PR when ready
|
||||||
|
3. Address review feedback
|
||||||
|
4. Merge PR (or approve auto-merge)
|
||||||
|
5. **That's it.** Everything else is automatic.
|
||||||
|
|
||||||
|
**What Ops/Brenda Sees:**
|
||||||
|
|
||||||
|
1. Slack notification: "✅ Deploy to production: ACME-X7K9"
|
||||||
|
2. Asana task auto-completes
|
||||||
|
3. Salesforce shows updated "Last Deploy" timestamp
|
||||||
|
4. No manual status updates needed
|
||||||
|
|
||||||
|
**When to Intervene:**
|
||||||
|
|
||||||
|
- Deploy fails (red X in GitHub Actions)
|
||||||
|
- Health check fails post-deploy
|
||||||
|
- Customer reports issue immediately after deploy
|
||||||
|
- → Use rollback workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metrics Dashboard
|
||||||
|
|
||||||
|
Track these in Salesforce or a monitoring tool:
|
||||||
|
|
||||||
|
| Metric | Target | Current |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| **Deploy Frequency** | > 5/week per project | - |
|
||||||
|
| **Lead Time** (commit → production) | < 30 minutes | - |
|
||||||
|
| **Change Failure Rate** | < 15% | - |
|
||||||
|
| **MTTR** (Mean Time to Recovery) | < 1 hour | - |
|
||||||
|
| **Deploy Success Rate** | > 95% | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
|
||||||
|
After each deploy, verify:
|
||||||
|
|
||||||
|
- [ ] CI pipeline passed all checks
|
||||||
|
- [ ] Backend health check returns 200
|
||||||
|
- [ ] Frontend loads without errors
|
||||||
|
- [ ] Database migrations applied (if any)
|
||||||
|
- [ ] Salesforce Project record updated
|
||||||
|
- [ ] Asana task marked complete
|
||||||
|
- [ ] Slack notification sent
|
||||||
|
- [ ] No alerts in monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely Cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| CI fails on test | Code issue | Fix tests, push again |
|
||||||
|
| Deploy fails on Railway | Token expired | Refresh `RAILWAY_TOKEN` secret |
|
||||||
|
| Salesforce not updating | Wrong project key | Verify repo name matches `blackroad-{PROJECT_KEY}-*` |
|
||||||
|
| Asana task not completing | Task name doesn't match | Ensure task name contains "Deploy" |
|
||||||
|
| Health check fails | Backend not fully started | Increase sleep time in workflow |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Docs
|
||||||
|
|
||||||
|
- [GitHub Actions: CI Workflow](../templates/github-actions/ci.yml)
|
||||||
|
- [GitHub Actions: Deploy Workflow](../templates/github-actions/deploy.yml)
|
||||||
|
- [GitHub Actions: Notify Stakeholders](../templates/github-actions/notify-stakeholders.yml)
|
||||||
|
- [Integration: GitHub → Salesforce](../integrations/github-to-salesforce.md)
|
||||||
|
- [New Client Kickoff Workflow](./new-client-kickoff.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
**"Deploy should be boring."**
|
||||||
|
|
||||||
|
The goal is to make deployments so reliable, automated, and well-monitored that they become **non-events**.
|
||||||
|
|
||||||
|
Every commit to `main` is a potential release. Every release updates all stakeholders automatically. No human should ever ask "Did this deploy?" or "What version is in production?"
|
||||||
|
|
||||||
|
The system should always know.
|
||||||
Reference in New Issue
Block a user