Chapter 18: A Professional Deployment Workflow with Git & CI/CD
Introduction: Beyond FTP
In the early days of web development, deploying a site was often a manual and nerve-wracking process of uploading files one by one via FTP. This approach is slow, prone to human error, and provides no easy way to roll back to a previous version if something goes wrong. For a professional, multi-site application, this is simply not a viable option.
This chapter introduces a modern, automated, and reliable workflow for deploying code. We will cover the fundamentals of Git, the industry-standard version control system that acts as the single source of truth for our code. We will then demonstrate how to utilise a CI/CD (Continuous Integration/Continuous Deployment) pipeline, using GitHub Actions as a practical example, to automate the testing and deployment of our code to the live server.
Adopting this workflow is a critical step in professional development. It makes deployments fast, predictable, and, most importantly, safe.
The Foundation: Version Control with Git
Git is a version control system that acts as a complete history of every change ever made to your code. Instead of having folders like project-v1, project-v2, and project-final-final, Git stores the history in a special hidden directory named .git inside your project root.
All our work is done on our local machines and then "pushed" to a central remote repository, which we host on a service like GitHub. This remote repository becomes the single source of truth for the entire project.
The basic workflow for a developer is a simple three-step process:
-
git add: After making changes to files, you tell Git to "stage" them, marking them to be included in the next snapshot of the project.
-
git commit: You save the staged changes as a snapshot in the project's history. Each commit has a unique ID and a descriptive message explaining what was changed.
-
git push: You upload your new commits from your local machine to the central remote repository, sharing your changes with the rest of the team.
By using Git, we have a complete, auditable history of our codebase, which is the essential foundation for building an automated deployment pipeline.
A Simple Branching Strategy
To manage development effectively, we use a simple branching model. A branch in Git is an independent line of development. This allows you to work on a new feature without affecting the stable, live version of your code.
Our model relies on two main types of branches:
main Branch: This branch is our definitive source of truth. The code in the main branch is always stable, tested, and represents what is currently running on our live production server. We never commit code directly to this branch.
feature Branches: All new work, from a small bug fix to a large new feature, is done on a dedicated feature branch (e.g., feature/add-contact-form or fix/user-login-bug). This work happens in isolation. Once the feature is complete and tested, it is merged into the main branch, ready for deployment.
This simple "mainline" branching strategy is powerful. It ensures our production code is always protected, while allowing for parallel development of multiple new features.
Automating Deployments with a CI/CD Pipeline
With our code safely in Git, we can now automate the deployment process. A CI/CD pipeline is an automated workflow that runs whenever you push changes to your remote repository.
CI (Continuous Integration): This is the first part of the pipeline. When a developer pushes a new commit, a remote server (called a "runner") automatically checks out the code and runs a series of quality checks, such as running an automated test suite. If any tests fail, the pipeline stops and notifies the developer, preventing bugs from being deployed.
CD (Continuous Deployment): If all the integration checks pass, the second part of the pipeline takes over. The runner executes a script that securely logs into our production server and deploys the latest version of the code from the main branch.
What is a "Runner"?
The "runner" is a virtual machine that executes the jobs in your workflow. The good news is that for most web projects, you do not need to create your own. When you use a service like GitHub Actions, they provide and manage a huge fleet of these runners for you. The line runs-on: ubuntu-latest in our configuration file is a direct instruction to GitHub to provide one of their standard, pre-configured virtual machines for our job. After the workflow is complete, this virtual machine is automatically destroyed. This is the standard, zero-maintenance approach for most deployment pipelines.
This entire process is defined in a simple text file (usually written in YAML) that lives inside our project repository. For our unified application, we use GitHub Actions, which is a CI/CD system built directly into GitHub.
A Practical Example: Deploying with GitHub Actions
Our entire CI/CD pipeline is defined in a single YAML file located at .github/workflows/deploy.yml in our project repository. This file tells GitHub what to do whenever new code is pushed to our main branch.
The process involves securely logging into our server and using a tool like rsync (a fast file transfer utility) to synchronize the code from the repository to the live web directory.
The GitHub Actions Workflow (deploy.yml) ' # .github/workflows/deploy.yml'
name: Deploy to Production
Trigger this workflow on any push to the 'main' branch on: push: branches:
- main
jobs: deploy: runs-on: ubuntu-latest
steps:
Step 1: Check out the repository's code
- name: Checkout code
uses: actions/checkout@v3
Step 2: Run Composer Install
This ensures all our PHP dependencies are installed
- name: Install PHP Dependencies
run: composer install --no-dev --optimize-autoloader
Step 3: Deploy files to the server
This uses a popular GitHub Action for rsync.
It securely connects using an SSH key stored in GitHub Secrets.
- name: Deploy files uses: appleboy/ssh-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | rsync -avz --delete ./ /var/www/oophp.uk/ --exclude '.git/' echo "Deployment successful!"
This workflow automates the entire deployment. When a developer merges a feature
A Robust Alternative: Generating YAML from PHP
Here is the draft for the next part of the chapter.
A Practical Example: A PHP-Driven Workflow While YAML is the standard format for GitHub Actions, its strict reliance on indentation can be fragile and lead to difficult-to-diagnose syntax errors. To create a more robust and familiar workflow, we can define our entire deployment pipeline in a native PHP array and then generate the final YAML file from that.
This approach gives us the full power and syntactic safety of PHP, completely eliminating any risk of whitespace errors.
- Defining the Pipeline in PHP First, we create a PHP script that defines the entire pipeline structure in an array. This file is easy to read, edit, and can be tracked in Git.
'File: build-pipeline.php'
<?php
require 'vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
// Define the entire workflow as a standard PHP array
pipeline=[
′
name
′
=
′
DeploytoProduction
′
,
′
on
′
=[
′
push
′
=[
′
branches
′
=[
′
main
′
]]],
′
jobs
′
=[
′
deploy
′
=[
′
runs−on
′
=
′
ubuntu−latest
′
,
′
steps
′
=[[
′
name
′
=
′
Checkoutcode
′
,
′
uses
′
=
′
actions/checkout@v3
′
],[
′
name
′
=
′
InstallPHPDependencies
′
,
′
run
′
=
′
composerinstall−−no−dev−−optimize−autoloader
′
],[
′
name
′
=
′
Deployfilesviarsync
′
,
′
uses
′
=
′
appleboy/ssh−action@master
′
,
′
with
′
=[
′
host
′
=
′
{{ secrets.SSH_HOST }}',
'username' => 'secrets.SSH_USERNAME
′
,
′
key
′
=
′
{{ secrets.SSH_PRIVATE_KEY }}',
'script' => 'rsync -avz --delete ./ /var/www/oophp.uk/ --exclude ".git/"'
]
]
]
]
]
];
// Convert the PHP array to a YAML string and save it
$yaml = Yaml::dump($pipeline, 4, 2);
file_put_contents('.github/workflows/deploy.yml', $yaml);
echo "deploy.yml file created successfully.\n";
```php
<?php
// File: build-pipeline.php
require 'vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
$pipeline = [
'name' => 'Deploy to Production',
'on' => [
'push' => ['branches' => ['main']]
],
'jobs' => [
'deploy' => [
'runs-on' => 'ubuntu-latest',
'steps' => [
['name' => 'Checkout code', 'uses' => 'actions/checkout@v3'],
['name' => 'Install PHP Dependencies', 'run' => 'composer install --no-dev --optimize-autoloader'],
[
'name' => 'Deploy files via rsync',
'uses' => 'appleboy/ssh-action@master',
'with' => [
'host' => '${{ secrets.SSH_HOST }}',
'username' => '${{ secrets.SSH_USERNAME }}',
'key' => '${{ secrets.SSH_PRIVATE_KEY }}',
'script' => 'rsync -avz --delete ./ /var/www/oophp.uk/ --exclude ".git/"'
]
]
]
]
]
];
$yaml = Yaml::dump($pipeline, 4, 2);
file_put_contents('.github/workflows/deploy.yml', $yaml);
2. The Workflow
With this setup, a developer never edits the fragile .yaml file directly. They modify the PHP array and run php build-pipeline.php to generate the final, perfectly formatted deploy.yaml file.
A Step-by-Step Walkthrough in PhpStorm
Let's walk through the process of deploying a small change using the IDE's interface.
Create a Feature Branch: In PhpStorm, click the current branch name and select "+ New Branch" (e.g., fix/template-typo).
Make and Commit Changes: Correct the typo. The changed file will appear in the "Commit" tool window. Write a commit message and click "Commit".
Push the Branch: Use the shortcut Ctrl+Shift+K (Windows/Linux) or Cmd+Shift+K (macOS) to push the new branch to GitHub.
Create a Pull Request: On GitHub, create a "Pull Request" to merge your branch into the main branch.
Merge and Deploy: Once approved, click "Merge". This automatically triggers our GitHub Actions pipeline, and the change is deployed in seconds.
Conclusion: Fast, Reliable, and Reversible Deployments
By combining Git with an automated CI/CD pipeline, we transform our deployment process into a fast, reliable operation. Every change is tied to a specific commit, providing a complete audit trail. Most importantly, this system is reversible. If a bug is deployed, we can simply revert the commit in Git, and the pipeline will automatically redeploy the last stable version.