mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
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.
304 lines
12 KiB
YAML
304 lines
12 KiB
YAML
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
|