Objective. Students create a GitHub Actions workflow that automatically runs linting and unit tests on push and deploys a passing build to their VPS via SSH on merge to the main branch.
This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.
VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.
Continuous Integration (CI) is the practice of automatically building and testing code every time a developer pushes a change. Continuous Delivery or Deployment (CD) extends that by automatically shipping code that passes all checks to a staging or production environment. Together, a CI/CD pipeline shortens the feedback loop between writing code and knowing whether it works — and whether it is live.
GitHub Actions is a CI/CD platform built directly into GitHub. You define workflows in YAML files stored in your repository under `.github/workflows/`. Each workflow describes when to run (triggers), what environment to use (runners), and what commands to execute (steps and jobs).
A GitHub Actions workflow file has four key sections:
A minimal Python CI workflow looks like this:
```yaml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
with:
python-version: '3.11'
```
The `uses` keyword pulls in a pre-built action from the GitHub Marketplace. `actions/checkout` fetches your repository code onto the runner. `actions/setup-python` installs a specific Python version.
Linting checks code style and common errors before tests even run. For Python projects, flake8 is the standard linter. For JavaScript projects, eslint serves the same role. Running a linter first catches formatting problems quickly and cheaply.
Unit tests verify that individual functions behave correctly. In Python the most common frameworks are pytest and the built-in `unittest` module. Both integrate with GitHub Actions the same way — run the test command as a step.
If any step exits with a non-zero status code, GitHub Actions marks that job as failed and stops running subsequent steps. This is the enforcement mechanism: a broken test prevents deployment.
The deployment job should only run when the CI job passes and when the trigger is a merge to the `main` branch. You control this with the `needs` keyword (to wait for the test job) and an `if` condition.
To deploy to your VPS over SSH, the community action `appleboy/ssh-action` is widely used. It opens an SSH session to your server and runs any shell commands you provide — typically `git pull` and a command to restart your server process (such as `systemctl restart myapp` or restarting a Gunicorn or Node process).
Because SSH requires a private key, you must never paste that key directly into a workflow file. Instead, store it as a GitHub Actions secret (`Settings → Secrets and variables → Actions → New repository secret`). Reference it in the workflow as `${{ secrets.VPS_KEY }}`. Secrets are masked in logs and are never exposed to pull requests from forks.
```yaml
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_KEY }}
script: |
cd /var/www/myapp
git pull origin main
systemctl restart myapp
```
Keep these points in mind whenever credentials or remote access are involved:
To confirm your pipeline enforces quality gates, you must deliberately break it. Introduce a syntax error or make a test assertion fail, push to a feature branch, and confirm that the Actions run shows a red failure on the test job and that the deploy job never starts. Then fix the code, push again, and verify the green run shows deployment completing. Capture screenshots of both runs in your lab log — this is the evidence that your pipeline actually blocks bad code.
Checking the Actions tab in your GitHub repository shows the full run history with each job's status, step-by-step logs, and timing. This is the primary tool you and an instructor use to verify pipeline behavior.
Answer each question, then check it. Nothing is recorded; this is your own practice.
A free tech program — complimentary, as our commitment to get America working. Help us build VoTechWorks. Tell us what works, what doesn’t, and what you want to learn.