Uni B Agency

v1.0 Mandatory Standard No Exceptions

CI/CD Standard Operating Procedure

Continuous Integration & Deployment with GitHub + cPanel — Uni B Agency Engineering Team

Authored by
Engineering Team, Uni B Agency
Applies to
All web projects hosted on cPanel
Status
Mandatory — No Exceptions

1. Philosophy & Non-Negotiables

The live server is read-only. GitHub is the single source of truth.

Every change — no matter how small — must flow through GitHub. A typo fix, a colour change, a config tweak: all of it goes through a commit, a pull request, and the automated pipeline. There are no exceptions.

Why This Matters

Direct server edits are the single biggest cause of:

  • Production outages that cannot be traced or rolled back
  • Security vulnerabilities introduced silently
  • Code drift where the server no longer matches what is in version control
  • Team confusion when developers pull “outdated” code

At Uni B Agency, we treat the live server the same way a pilot treats a plane’s controls — we follow a checklist, every time, without shortcuts.

The Three Guarantees This Process Gives Us

01
Traceability

Every change has an author, a timestamp, and a reason.

02
Rollback

Any broken deployment can be reversed with one git revert.

03
Consistency

What runs locally is what runs in production.

2. Branching Strategy

We use a structured three-tier branching model.

main          <-- production server deploys from here (PROTECTED)
  ^
develop       <-- integration branch, staging deploys from here
  ^
feature/*     <-- individual developer work
hotfix/*      <-- emergency production patches only

main — Production Branch

  • Protected. No one pushes directly to main, ever. Not even the lead developer.
  • Reflects exactly what is running on the live server.
  • Code enters main only via a reviewed, approved Pull Request from develop.
  • Every merge triggers an automatic deployment to production.

develop — Integration Branch

  • Protected. No direct pushes.
  • Where completed features are collected and tested before going to production.
  • All developers merge their finished feature branches via Pull Requests.
  • If a staging server exists, develop deploys there automatically.

feature/[name] — Feature Branches

  • Created by individual developers for each piece of work.
  • Naming: feature/task-description (e.g. feature/add-payment-gateway)
  • Always branched off develop, never off main.
  • Deleted after the PR is merged.

hotfix/[name] — Emergency Patches

  • Used only when a critical bug on production must be fixed immediately.
  • Branched off main, merged into BOTH main AND develop when complete.
  • Naming: hotfix/brief-description (e.g. hotfix/login-redirect-broken)

Branch Protection Rules

Configure in GitHub: Repository → Settings → Branches

BranchRules
mainRequire PR, require 1 approval, require status checks to pass, no force push, no deletion
developRequire PR, require status checks to pass, no force push

3. Repository Setup

3.1 Creating a New Project Repository

All Uni B Agency project repositories are private by default, live under the UNI-B-AGENCY GitHub organisation, and follow the naming convention project-domain.

# On your local machine
cd /path/to/project

git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin git@github.com:UNI-B-AGENCY/[project-name].git
git push -u origin main

# Create the develop branch immediately
git checkout -b develop
git push -u origin develop

3.2 Required Files in Every Repository

FilePurpose
.gitignoreExcludes secrets, vendor files, logs, OS files
.env.exampleDocuments all required env variables (no real values)
.cpanel.ymlTells cPanel what commands to run after deployment
.github/workflows/deploy.ymlGitHub Actions CI/CD pipeline
README.mdProject description, local setup instructions

3.3 Standard .gitignore Entries

# Secrets -- never commit these
.env
.env.*
!.env.example

# Dependencies -- installed on deploy
/vendor
/node_modules

# Laravel runtime files
/bootstrap/cache/*
/storage/*.key
/storage/installed
/storage/framework/cache/*
/storage/framework/sessions/*
/storage/framework/views/*
/storage/logs/*

# Server-specific files -- never commit these
/cgi-bin/
error_log
*.sql
/public/user-uploads/*
!public/user-uploads/.htaccess

# Local dev tools
/.cursor/
/.idea/
/.vscode/
*.code-workspace
.DS_Store
Thumbs.db

4. cPanel Server Setup

One-time setup per project, performed by the lead developer.

4.1 Document Root Configuration

Critical for Laravel projects

The web server must serve from public/, not the project root. Failure to do this exposes your .env file and source code to the internet.

  1. cPanel → Domains (or Subdomains)
  2. Find your domain → click Manage
  3. Set Document Root to: /home/[cpanel-username]/[project-folder]/public
  4. Save

4.2 SSH Key for GitHub Access

Run in cPanel Terminal:

# Generate a dedicated deploy key (no passphrase -- required for automation)
ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -N ""

# Create SSH config so git uses this key for GitHub
cat > ~/.ssh/config << 'EOF'
Host github.com
    IdentityFile ~/.ssh/github_deploy
    User git
EOF

chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/github_deploy

# Print the public key to add to GitHub
cat ~/.ssh/github_deploy.pub

In GitHub: Repository → Settings → Deploy keys → Add deploy key. Title: cPanel Production Server. Do NOT grant write access.

ssh -T git@github.com
# Expected: Hi UNI-B-AGENCY/[repo]! You've successfully authenticated...

4.3 Clone via cPanel Git Version Control

  1. cPanel → FilesGit™ Version Control
  2. Click Create → enable Clone a Repository
  3. Clone URL: git@github.com:UNI-B-AGENCY/[repo].git
  4. Repository Path: /home/[cpanel-username]/[project-folder] (use a name that does NOT match the subdomain folder)
  5. Click Create and accept the GitHub SSH host key

4.4 SSH Key for GitHub Actions

# Generate a separate key for GitHub Actions
ssh-keygen -t ed25519 -f ~/.ssh/github_actions_deploy -N ""

# Authorise it for SSH login
cat ~/.ssh/github_actions_deploy.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

# Print the PRIVATE key -- this goes into GitHub Secrets
cat ~/.ssh/github_actions_deploy

Add to GitHub Secrets: Repository → Settings → Secrets and variables → Actions

Secret NameValue
SSH_HOSTYour server hostname (e.g. s10701.hostingprovider.com)
SSH_USERNAMEYour cPanel username (e.g. unibagency)
SSH_PRIVATE_KEYThe entire private key including -----BEGIN and -----END lines
SSH_PORT22 (confirm with your host if different)

4.5 Initial Laravel Setup on the Server

Run once after the first clone, before the site goes live:

cd ~/[project-folder]

# Create production environment file
cp .env.example .env
nano .env   # Fill in all production values

# Install dependencies
/usr/local/bin/composer install --no-dev --optimize-autoloader

# Generate application key
php artisan key:generate

# Run database migrations
php artisan migrate --force

# Create storage symlink
php artisan storage:link

# Cache configuration for performance
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Set permissions
chmod -R 775 storage bootstrap/cache

5. GitHub Actions CI/CD Pipeline

5.1 Standard Workflow File

Save as .github/workflows/deploy.yml in every project:

name: Deploy to Production

on:
  push:
    branches:
      - main          # Only deploys when code reaches main

jobs:
  deploy:
    name: SSH Deploy to cPanel
    runs-on: ubuntu-latest

    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            set -e   # Stop on any error

            cd ~/[project-folder]

            # Force-align server with GitHub (discards any server-side drift)
            git fetch origin
            git reset --hard origin/main

            # Install/update PHP dependencies
            /usr/local/bin/composer install --no-dev --optimize-autoloader

            # Maintenance mode: allow only server IP
            php artisan down --allow=127.0.0.1

            # Run any pending database migrations
            php artisan migrate --force

            # Rebuild caches
            php artisan config:cache
            php artisan route:cache
            mkdir -p resources/views/modules/sms
            php artisan view:cache

            # Ensure storage link exists
            php artisan storage:link --force

            # Bring site back online
            php artisan up

            echo "Deployment complete."

5.2 What Each Step Does

StepPurpose
git reset --hard origin/mainForces the server to match GitHub exactly — prevents local drift
composer install --no-devInstalls PHP packages, skipping development-only tools
artisan downShows a maintenance page to users during deployment
artisan migrate --forceApplies any new database schema changes
artisan config:cacheCompiles .env into a cached file for faster performance
artisan route:cacheCompiles all routes into a single cached file
artisan view:cachePre-compiles all Blade templates
artisan upBrings the site back online

5.3 The set -e Directive

set -e means: if any command fails, stop immediately. This prevents a broken migration from being followed by a cache rebuild that hides the error. Always keep this line.

6. The Deployment Workflow

This is the day-to-day process every developer at Uni B Agency follows.

Normal Feature Development

1
Pull latest develop
git checkout develop && git pull origin develop
2
Create your feature branch
git checkout -b feature/your-feature-name
3
Do your work, commit often
git add [specific files]
git commit -m "Short description of what and why"
4
Push your branch to GitHub
git push origin feature/your-feature-name
5
Open a Pull Request on GitHub
Base: develop ← Compare: feature/your-feature-name. Add a clear description of what changed and why.
6
Request a code review from a teammate
7
Address review comments, push fixes to the same branch
8
Once approved and CI passes → Merge into develop
9
Delete the feature branch (GitHub prompts you after merge)

Releasing to Production

1
Open a Pull Request: Base: main ← Compare: develop
2
Title it clearly: “Release [date] — [brief summary of what’s included]”
3
The lead developer reviews the PR
4
Once approved → Merge into main
5
GitHub Actions fires automatically
6
Watch the Actions tab — confirm the workflow goes green
7
Spot-check the live site to verify the change is visible

Commit Message Standard

[type]: short description (max 72 characters)

Optional: longer explanation of WHY this change was made.
TypeWhen to use
feat:New feature
fix:Bug fix
style:CSS/UI change with no logic change
refactor:Code restructure, no behaviour change
docs:Documentation only
chore:Dependency updates, config changes
hotfix:Emergency production fix
feat: add phone and email to landing page footer
fix: correct task submission file URL using asset_url_local_s3
hotfix: resolve login redirect loop on production
chore: update composer dependencies for security patches

7. Security Standards

Security is not optional. Every developer is responsible for the security of code they commit.

7.1 Secrets Management

Never commit secrets.

Database passwords, API keys, app encryption keys (APP_KEY), payment credentials, SMTP passwords, and any third-party service credentials must never be committed.

All secrets live in the .env file on the server. The .env file is in .gitignore and must never be committed. For GitHub Actions, use GitHub’s encrypted Secrets — never hardcode values in the workflow YAML.

7.2 API Token Rotation

cPanel API tokens must be rotated every 90 days or immediately after any team member leaves.

  1. cPanel → Manage API Tokens → Create new token
  2. Update the GitHub Secret with the new token
  3. Delete the old token in cPanel
  4. Confirm the next deployment succeeds before closing the session

7.3 Server File Monitoring

Periodically scan the server for files that do not belong:

# Find PHP files in non-PHP directories (potential webshells)
find ~/[project]/public/css -name "*.php"
find ~/[project]/public/js -name "*.php"
find ~/[project]/documentation -name "*.php"

# Find recently modified PHP files (last 7 days)
find ~/[project]/public -name "*.php" -mtime -7

# Find world-writable files (security risk)
find ~/[project] -perm -002 -type f

Red flags — investigate immediately:

  • PHP files inside css/, js/, img/, or documentation/ directories
  • Short random filenames like n33.php, ws87.php, x7.php
  • Text files like trb.txt, test.txt in the public directory
  • Files containing eval(, base64_decode(, system(, exec(

If suspicious files are found:

  1. Read the file content before deleting
  2. If it contains obfuscated code — change all passwords immediately (cPanel, database, GitHub, email)
  3. Check .htaccess files for injected redirect rules
  4. Delete the files and audit server access logs
  5. Report to the team lead

7.4 Production Environment Settings

APP_ENV=production
APP_DEBUG=false          # NEVER true on production -- exposes stack traces
APP_URL=https://your-domain.com
REDIRECT_HTTPS=true

APP_DEBUG=true on a live server exposes database credentials, file paths, and internal logic to anyone who triggers an error. This is a critical vulnerability.

7.5 File Permissions

# Application directories
chmod -R 755 ~/[project]

# Storage and cache -- must be writable
chmod -R 775 ~/[project]/storage
chmod -R 775 ~/[project]/bootstrap/cache

# Environment file -- owner only
chmod 600 ~/[project]/.env

7.6 Dependency Security

# Check for known PHP vulnerabilities
composer audit

# Check for known JS vulnerabilities
npm audit

Block any merge that introduces a package with a critical severity vulnerability.

8. Environment Configuration

8.1 The Environment Hierarchy

EnvironmentBranchServer.env managed by
Local developmentfeature/*Developer’s machineDeveloper
Staging (if available)developStaging cPanelLead developer
ProductionmainProduction cPanelLead developer only

8.2 What Lives in .env.example

This file is the contract. It must list every variable the application needs with safe placeholder values. New developers cp .env.example .env and fill in their local values.

APP_NAME=ProjectName
APP_KEY=                          # Generate with: php artisan key:generate
APP_ENV=local                     # Change to: production on server
APP_DEBUG=true                    # Change to: false on server
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database_name
DB_USERNAME=your_db_user
DB_PASSWORD=

MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@unibagency.com
MAIL_FROM_NAME="${APP_NAME}"

8.3 Production .env Handoff

When a new project goes live, the lead developer:

  1. Sets the production .env on the server directly via cPanel Terminal
  2. Runs php artisan config:cache to apply it
  3. Does not share the production .env in Slack, email, or any chat system
  4. Stores it securely in the team’s password manager (e.g. Bitwarden, 1Password)

9. Emergency Hotfix Procedure

When production is broken and cannot wait for the normal cycle:

# 1. Branch off main (NOT develop)
git checkout main
git pull origin main
git checkout -b hotfix/brief-description

# 2. Make the minimum fix required -- nothing more

# 3. Commit with a clear message
git add [specific files]
git commit -m "hotfix: [description of what was broken and what was fixed]"

# 4. Push and open a PR directly to main
git push origin hotfix/brief-description
# Open PR: main <-- hotfix/brief-description
# Get one approval (can be fast-tracked in a genuine emergency)

# 5. Merge to main -- triggers automatic deployment
# Verify the fix on the live site

# 6. IMPORTANT: merge the hotfix into develop too
git checkout develop
git pull origin develop
git merge hotfix/brief-description
git push origin develop

# 7. Delete the hotfix branch
git branch -d hotfix/brief-description
git push origin --delete hotfix/brief-description

Step 6 is mandatory.

If you do not merge the hotfix into develop, the next release from develop → main will revert your fix.

10. Onboarding a New Project

Checklist for setting up a brand new project under this CI/CD standard:

  • Create private repository under UNI-B-AGENCY organisation
  • Add .gitignore, .env.example, README.md in first commit
  • Create and protect both main and develop branches
  • Set up cPanel subdomain with correct document root (pointing to /public)
  • Generate GitHub deploy key on cPanel, add to GitHub Deploy Keys
  • Clone repository via cPanel Git Version Control
  • Create .cpanel.yml with deployment tasks
  • Run initial server setup (composer install, migrate, storage:link)
  • Generate GitHub Actions SSH key, add to authorized_keys
  • Add SSH_HOST, SSH_USERNAME, SSH_PRIVATE_KEY, SSH_PORT to GitHub Secrets
  • Create .github/workflows/deploy.yml
  • Push to main, verify GitHub Action runs green
  • Spot-check live site
  • Document project-specific notes in README.md
  • Scan server for unexpected files one week after launch

11. Onboarding a New Developer

What they receive

  • Read access to the GitHub repository
  • Local .env values from the lead developer (never production values)
  • This document

What they never receive

  • Production .env contents
  • cPanel login credentials
  • Server SSH private keys
  • GitHub API tokens with write access to main

GitHub Authentication Setup (Do This First)

Before you can push or clone, your local machine needs to authenticate with GitHub. Choose one method:

Method A — SSH Key (Recommended for daily use)

# 1. Check if you already have an SSH key
cat ~/.ssh/id_ed25519.pub
# If it prints a key starting with ssh-ed25519, copy it and skip to step 3.
# If you get "No such file or directory", generate one:

# 2. Generate a new SSH key
ssh-keygen -t ed25519 -C "your_email@example.com"
# Press Enter through all prompts to accept the default location

# 3. Print the public key and copy the output
cat ~/.ssh/id_ed25519.pub

In GitHub: click your profile icon → SettingsSSH and GPG keysNew SSH key. Paste the key and save.

Use the SSH clone URL: git@github.com:UNI-B-AGENCY/[repo].git

Method B — HTTPS + Personal Access Token (Simpler to start)

Important: GitHub no longer accepts your account password for Git operations. You must use a Personal Access Token (PAT) instead.

Generate a PAT: GitHub → Profile icon → SettingsDeveloper settingsPersonal access tokensTokens (classic)Generate new token. Grant the repo scope. Save the token — you will not see it again.

Use the HTTPS clone URL: https://github.com/UNI-B-AGENCY/[repo].git. When Git prompts for a password, enter your PAT, not your GitHub password.

Local Setup

# Clone the repository (SSH)
git clone git@github.com:UNI-B-AGENCY/[repo].git

# -- OR -- clone via HTTPS (if using Method B above)
git clone https://github.com/UNI-B-AGENCY/[repo].git
cd [repo]

# Set up environment
cp .env.example .env
# Fill in local database and other local credentials

# Install dependencies
composer install
npm install

# Set up the database
php artisan migrate
php artisan db:seed  # if seeders exist

# Start the local server
php artisan serve

Daily Workflow Reminder

Pull develop → Create feature branch → Work → Commit → Push → Open PR → Get reviewed → Merge to develop

They never touch main directly. Ever.

12. Forbidden Practices

The following actions are prohibited at Uni B Agency. Violation will result in an incident review.

Forbidden ActionWhy
Editing files directly on the live server (via File Manager, FTP, or nano)Creates untracked changes that break future deployments and cannot be rolled back
Committing .env filesExposes credentials to anyone with repository access
Committing vendor/ or node_modules/Creates enormous repositories and masks dependency version issues
Force-pushing to main or developCan permanently erase teammates’ work and the deployment history
Sharing production credentials in Slack, WhatsApp, or emailCredentials shared in plain text are credentials compromised
Deploying on a Friday afternoonNo coverage if something breaks over the weekend
Skipping code review for “small” changesThe most catastrophic bugs are the ones that looked small
Using APP_DEBUG=true on a live serverExposes stack traces, file paths, and database credentials to the public
Committing database dumps (.sql files)Contains real user data; violates data privacy obligations
Running php artisan migrate:fresh on productionDrops all tables and destroys all data — irreversible

13. Troubleshooting Reference

GitHub Actions is green but server code did not update

Cause: Uncommitted changes on the server blocked git pull.

cd ~/[project] && git status

git fetch origin
git reset --hard origin/main

Re-trigger the deployment after the fix. This is why git reset --hard origin/main is in the standard deploy script.

GitHub Actions fails with Permission denied (publickey)

Cause: The SSH private key in GitHub Secrets does not match the public key in ~/.ssh/authorized_keys on the server.

  1. On the server: cat ~/.ssh/authorized_keys — confirm your key is there
  2. In GitHub Secrets: confirm SSH_PRIVATE_KEY contains the matching private key including header/footer lines

cPanel API returns 415 Unsupported Media Type

Cause: Sending data as a POST form body. cPanel’s UAPI expects GET with query parameters.

GET https://[host]:2083/execute/VersionControl/update?repository_root=/home/[user]/[project]

cPanel Deploy button is greyed out

Cause: Either .cpanel.yml is missing or there are uncommitted changes on the server.

cat ~/[project]/.cpanel.yml    # Must exist
git status                      # Must show "nothing to commit"

php artisan view:cache fails with “directory does not exist”

Cause: A module references a view directory not yet created on the server.

mkdir -p resources/views/modules/sms
php artisan view:cache

The GET method is not supported for route /

Cause: Route cache is stale or corrupt after a failed partial deployment.

php artisan route:clear
php artisan config:clear
php artisan cache:clear
# Then re-run caches:
php artisan config:cache
php artisan route:cache
php artisan view:cache

Storage link error: [public/storage] link already exists

This is not an error — it is a warning. The --force flag handles it:

php artisan storage:link --force

Already included in the standard deploy script.

Application shows the install wizard on a fresh deploy

Cause: The storage/installed flag file does not exist. It is in .gitignore by design and must be created manually on each new server.

# Run once per server -- never commit
touch ~/[project]/storage/installed
php artisan config:clear

Permission denied (publickey) when pushing from your local machine

Cause: GitHub does not recognise your local SSH key. This is different from the GitHub Actions → cPanel error above; this happens on a developer’s own machine.

Fix A — Add your SSH key to GitHub:

# Check if you have an existing key
cat ~/.ssh/id_ed25519.pub

# If "No such file or directory", generate one first:
ssh-keygen -t ed25519 -C "your_email@example.com"
# Then print it:
cat ~/.ssh/id_ed25519.pub

Copy the output. Go to GitHub → Settings → SSH and GPG keys → New SSH key, paste it, and save. Then retry your push.

Fix B — Switch to HTTPS and use a Personal Access Token:

# Switch your remote from SSH to HTTPS
git remote set-url origin https://github.com/UNI-B-AGENCY/[repo].git

# Push (GitHub will prompt for credentials)
git push -u origin main

When Git prompts for a password, use a Personal Access Token (PAT) — not your GitHub account password. Generate one at: GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic). Grant the repo scope.

fatal: remote origin already exists

Cause: You ran git remote add origin but a remote named origin is already set (e.g. pointing at the wrong URL or a typo in the repo name).

Fix — update the URL instead of adding:

# Use set-url to overwrite the existing remote
git remote set-url origin git@github.com:UNI-B-AGENCY/[repo].git

# Verify it is correct
git remote -v

# Then push
git push -u origin main

Common mistake: a typo in the repo name (e.g. devedocs instead of devdocs). Always double-check the URL with git remote -v before pushing.

This document is a living standard. Propose changes via a Pull Request to the documentation branch with a clear explanation. All changes require sign-off from the Engineering Lead.

Last updated: