From c4flow
Set up GitHub Actions CI/CD and deploy the application to the EC2 infrastructure provisioned by c4flow:infra. Use when the user runs /c4flow:deploy or when the orchestrator advances to DEPLOY state. Requires infra to be provisioned first.
How this skill is triggered — by the user, by Claude, or both
Slash command
/c4flow:deployThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Phase**: 6: Release
Phase: 6: Release Agent type: Main agent (interactive) Status: Implemented
Generates a GitHub Actions SSH deploy workflow, commits it to the repo, triggers the first deployment, and monitors the run to completion.
Read docs/c4flow/.state.json. Check:
NGINX_OK=$(jq -r '.infraState.nginxConfigured // false' docs/c4flow/.state.json 2>/dev/null)
SECRETS_OK=$(jq -r '.infraState.githubSecretsConfigured // false' docs/c4flow/.state.json 2>/dev/null)
FQDN=$(jq -r '.infraState.fqdn // empty' docs/c4flow/.state.json 2>/dev/null)
If infraState is absent or either flag is not true:
Infrastructure not provisioned. Run /c4flow:infra first.
Required:
infraState.nginxConfigured: {value or "missing"}
infraState.githubSecretsConfigured: {value or "missing"}
Exit without proceeding.
Instead of interpolating the start command into the Actions YAML (injection risk), write a
scripts/deploy-start.sh to the repository. The workflow calls this script — keeping shell
logic out of the YAML and in a versioned, reviewable file.
mkdir -p scripts
detect_app_type() {
if [ -f "package.json" ]; then
echo "node"
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
echo "python"
elif [ -f "go.mod" ]; then
echo "go"
else
echo "generic"
fi
}
APP_TYPE=$(detect_app_type)
case "$APP_TYPE" in
node)
cat > scripts/deploy-start.sh <<'EOF'
#!/bin/bash
set -euo pipefail
cd ~/app
npm install --production
pm2 restart app 2>/dev/null || pm2 start npm --name app -- start
EOF
;;
python)
cat > scripts/deploy-start.sh <<'EOF'
#!/bin/bash
set -euo pipefail
# PREREQUISITE: a systemd unit file /etc/systemd/system/app.service must exist on the EC2 instance.
# Create it manually or via cloud-init before the first deploy.
cd ~/app
pip install -r requirements.txt
sudo systemctl restart app
EOF
;;
go)
cat > scripts/deploy-start.sh <<'EOF'
#!/bin/bash
set -euo pipefail
# PREREQUISITE: a systemd unit file /etc/systemd/system/app.service must exist on the EC2 instance.
# Create it manually or via cloud-init before the first deploy.
cd ~/app
go build -o app .
sudo systemctl restart app
EOF
;;
*)
cat > scripts/deploy-start.sh <<'EOF'
#!/bin/bash
set -euo pipefail
cd ~/app
# TODO: Add your application start command here
sudo systemctl restart app
EOF
echo "NOTE: Generic entrypoint written to scripts/deploy-start.sh."
echo "Review and update the start command before the first deploy."
;;
esac
chmod +x scripts/deploy-start.sh
echo "Entrypoint script written: scripts/deploy-start.sh (app type: $APP_TYPE)"
The workflow calls scripts/deploy-start.sh over SSH — no shell command interpolation in the YAML.
The appleboy/ssh-action is pinned to a commit SHA to prevent supply-chain attacks via mutable tags.
mkdir -p .github/workflows
cat > .github/workflows/deploy.yml <<'EOF'
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy to EC2
# Pinned to SHA — mutable tags (v1.2.5) can be silently updated by the author.
# This SHA corresponds to appleboy/ssh-action v1.2.5.
# To update: verify the new SHA at https://github.com/appleboy/ssh-action
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: ${{ secrets.AWS_EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
timeout: 300s
script: |
set -euo pipefail
# Pull latest code
if [ -d ~/app ]; then
cd ~/app && git fetch origin main && git reset --hard origin/main
else
git clone https://github.com/${{ github.repository }} ~/app && cd ~/app
fi
# Run the versioned entrypoint script
bash ~/app/scripts/deploy-start.sh
- name: Health Check
run: |
echo "Waiting for app to start..."
sleep 10
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 30 \
--max-filesize 1024 \
--retry 5 \
--retry-delay 5 \
"https://${{ secrets.DEPLOY_DOMAIN }}/")
echo "Health check status: $STATUS"
if [ "$STATUS" != "200" ] && [ "$STATUS" != "301" ] && [ "$STATUS" != "302" ]; then
echo "Health check failed (HTTP $STATUS)"
exit 1
fi
echo "Deploy successful! Live at: https://${{ secrets.DEPLOY_DOMAIN }}"
EOF
git add .github/workflows/deploy.yml scripts/deploy-start.sh
git diff --cached --stat
git commit -m "ci: add GitHub Actions deploy workflow and entrypoint script"
git push origin main
echo "Workflow committed and pushed to main."
echo "Waiting for GitHub Actions to pick up the workflow run..."
sleep 8
# Get the latest run ID for the deploy workflow
RUN_ID=$(gh run list --workflow=deploy.yml --limit=1 --json databaseId --jq '.[0].databaseId' 2>/dev/null)
if [ -z "$RUN_ID" ]; then
# Trigger manually if not auto-triggered
gh workflow run deploy.yml
sleep 8
RUN_ID=$(gh run list --workflow=deploy.yml --limit=1 --json databaseId --jq '.[0].databaseId')
fi
echo "Monitoring workflow run $RUN_ID ..."
echo "(Press Ctrl+C to stop monitoring — the deploy continues in the background)"
echo ""
gh run watch "$RUN_ID"
RUN_STATUS=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion' 2>/dev/null)
FQDN=$(jq -r '.infraState.fqdn' docs/c4flow/.state.json)
if [ "$RUN_STATUS" = "success" ]; then
echo ""
echo "=== Deploy Succeeded ==="
echo "Live at: https://$FQDN"
echo ""
echo "The c4flow workflow is complete."
# Orchestrator advances currentState to DONE
else
echo ""
echo "=== Deploy Failed ==="
echo ""
echo "SECURITY NOTE: The following log excerpt may contain application output."
echo "Do not share it publicly if your app logs credentials or PII."
echo ""
echo "--- Last 20 lines of failed steps ---"
# Fetch only failed step logs, limit size to avoid context bloat
gh run view "$RUN_ID" --log-failed 2>/dev/null | tail -20 || \
gh run view "$RUN_ID" --log 2>/dev/null | tail -20
echo "--- end of log excerpt ---"
echo ""
echo "Options:"
echo " 1. Fix the issue and push to main (workflow re-runs automatically)"
echo " 2. Re-run the failed job: gh run rerun $RUN_ID --failed"
echo " 3. Full logs: gh run view $RUN_ID --log"
echo ""
echo "State NOT advanced — re-run /c4flow:deploy once the issue is fixed."
fi
On success: the orchestrator adds DEPLOY to completedStates and sets currentState to DONE.
On failure: state is not advanced. User fixes the issue and re-runs /c4flow:deploy.
scripts/deploy-start.sh, not the workflow${{ secrets.* }} references onlyinfraState.nginxConfigured or githubSecretsConfigured is not true, exit immediatelygh run watch interruption is safe — the GitHub Actions run continues in the cloud regardlessnpx claudepluginhub tunneleven/c4flow --plugin c4flowScaffolds test + deploy CI/CD pipelines for GitHub Actions, GitLab CI, Jenkins, and targets like Vercel, Netlify, Docker after assessing user's git host and deploy setup. Teaches basics to beginners.
Build a full CI/CD pipeline from scratch. Use when asked to "set up CI/CD", "create pipeline", or "automate deploys".
Generates complete GitHub Actions CI/CD pipelines from project analysis, including lint, test, build, and deploy stages for Node.js, Python, Go, Rust, Java stacks. Use for setting up CI/CD or automating deploys.