Difficulty: Intermediate | Time: 45 minutes | Focus: GitHub Actions, image building, testing, vulnerability scanning
Objectives
By the end of this lab, you will understand how to set up a GitHub Actions workflow for container image building. You will implement automated testing of the built image to verify functionality. You will integrate vulnerability scanning into the CI/CD pipeline to catch security issues early. You will push images to a container registry for deployment. You will understand GitHub Actions workflow triggers and how jobs execute. You will be able to monitor CI/CD job execution and debug issues that arise.
Prerequisites
Required: You need a GitHub account with a repository you can push to. You need basic Git knowledge including clone, push, and commit operations. You need Docker CLI installed locally for testing. You need a text editor for writing YAML files.
Optional: The gh CLI (GitHub CLI) helps you manage workflows from the terminal. Container registry access (Docker Hub, GitHub Container Registry, etc.) allows you to push images.
GitHub Setup: Create a new GitHub repository or use an existing one. Clone it locally using git clone https://github.com/YOUR-USERNAME/YOUR-REPO.git. Navigate into the repo directory with cd YOUR-REPO.
Step 1: Create Project Files
Create the project structure with mkdir -p .github/workflows and mkdir -p app.
Create the Python application (app/main.py):
cat > app/main.py << 'EOF'from http.server import HTTPServer, BaseHTTPRequestHandlerfrom datetime import datetimeimport jsonimport sys class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/' or self.path == '/api': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { "service": "lab-06-cicd", "timestamp": datetime.now().isoformat(), "version": "1.0.0" } self.wfile.write(json.dumps(response).encode()) elif self.path == '/health': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status": "ok"}') else: self.send_response(404) self.end_headers() def log_message(self, format, *args): pass # Suppress default logging if __name__ == '__main__': server = HTTPServer(('0.0.0.0', 8000), Handler) print("Server running on port 8000") server.serve_forever()EOFCreate the Dockerfile:
cat > Dockerfile << 'EOF'FROM registry.cleanstart.com/cleanstart/python:3.12 WORKDIR /app COPY app/main.py . EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 CMD ["python", "main.py"]EOFCreate a test script (app/test.py):
cat > app/test.py << 'EOF'#!/usr/bin/env python3import jsonimport urllib.requestimport timeimport sys def test_endpoint(url, expected_status=200): """Test an endpoint and return True if successful""" try: with urllib.request.urlopen(url) as response: status = response.status content = response.read().decode() success = status == expected_status print(f" {url}: {'✅' if success else '❌'} ({status})") if status == expected_status and '{"' in content: print(f" Response: {content[:80]}...") return success except Exception as e: print(f" {url}: ❌ ({e})") return False def main(): print("Running integration tests...") # Wait for server to be ready (in Docker) time.sleep(1) tests = [ ("http://localhost:8000/", 200), ("http://localhost:8000/api", 200), ("http://localhost:8000/health", 200), ("http://localhost:8000/notfound", 404), ] results = [test_endpoint(url, status) for url, status in tests] if all(results): print("\n✅ All tests passed!") return 0 else: print("\n❌ Some tests failed!") return 1 if __name__ == '__main__': sys.exit(main())EOF chmod +x app/test.pyCreate .gitignore:
cat > .gitignore << 'EOF'.DS_Store*.pyc__pycache__/.venv/venv/*.logEOFStep 2: Create the GitHub Actions Workflow
Create .github/workflows/build.yml:
cat > .github/workflows/build.yml << 'EOF'name: Build and Test on: push: branches: - main - develop pull_request: branches: - main env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: name: Build Docker Image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build Docker image uses: docker/build-push-action@v5 with: context: . push: false tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} outputs: type=docker,dest=/tmp/image.tar - name: Upload image artifact uses: actions/upload-artifact@v3 with: name: docker-image path: /tmp/image.tar test: name: Test Docker Image runs-on: ubuntu-latest needs: build steps: - name: Checkout code uses: actions/checkout@v4 - name: Download image artifact uses: actions/download-artifact@v3 with: name: docker-image path: /tmp - name: Load Docker image run: | docker load --input /tmp/image.tar - name: Run container run: | docker run -d \ --name test-container \ -p 8000:8000 \ ${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Wait for container run: | for i in {1..30}; do if curl -f http://localhost:8000/health > /dev/null 2>&1; then echo "✅ Container is healthy" exit 0 fi sleep 1 done echo "❌ Container failed to become healthy" exit 1 - name: Run tests run: | python3 app/test.py - name: Container logs if: always() run: | docker logs test-container || true - name: Stop container if: always() run: | docker stop test-container || true scan: name: Security Scan runs-on: ubuntu-latest needs: build steps: - name: Checkout code uses: actions/checkout@v4 - name: Download image artifact uses: actions/download-artifact@v3 with: name: docker-image path: /tmp - name: Load Docker image run: | docker load --input /tmp/image.tar - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' continue-on-error: true - name: Upload Trivy results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' if: always() push: name: Push to Registry runs-on: ubuntu-latest needs: [build, test, scan] if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push image uses: docker/build-push-action@v5 with: context: . push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} EOFStep 3: Commit and Push to GitHub
Initialize Git if not already done:
git initgit config user.email "you@example.com"git config user.name "Your Name"Add all files:
git add .Create initial commit:
git commit -m "Lab 06: CI/CD pipeline with GitHub Actions"Add remote and push (replace with your repo):
git remote add origin https://github.com/YOUR-USERNAME/YOUR-REPO.gitgit branch -M maingit push -u origin mainYou may be prompted for GitHub credentials. Use a personal access token or SSH key.
Step 4: Monitor Workflow Execution
Go to your GitHub repository and click the Actions tab. You should see the workflow running.
The expected flow proceeds through four distinct stages. The build job starts first and builds your Docker image. Once the build completes successfully, the test job starts and runs tests on the image. The scan job starts in parallel and scans for vulnerabilities. Finally, the push job starts only on the main branch and pushes the image to the registry.
Monitor a specific job using GitHub CLI (optional):
# Using GitHub CLI (optional)gh run list --repo YOUR-USERNAME/YOUR-REPOgh run view <RUN_ID>Step 5: View Workflow Results
In the GitHub Actions tab, click on the latest workflow run. Expand each job to see detailed logs. Check for passing or failing steps that indicate which parts of the pipeline worked.
Expected output in build job:
#1 Building Docker image...#2 Dockerfile layers...Expected output in test job:
✅ Container is healthy http://localhost:8000/: ✅ (200) http://localhost:8000/api: ✅ (200) http://localhost:8000/health: ✅ (200) http://localhost:8000/notfound: ✅ (404) ✅ All tests passed!Step 6: Create a Pull Request to Test
Create a feature branch:
git checkout -b feature/test-ciMake a small change (e.g., update a comment):
echo "# Updated: Lab 06 CI/CD Demo" >> README.mdgit add README.mdgit commit -m "Test CI/CD workflow"Push and create a pull request:
git push origin feature/test-ciGo to GitHub and create a PR from feature/test-ci to main.
The workflow will run on the PR (build, test, scan jobs) but NOT the push job (since it's not merging to main).
Step 7: Customize the Workflow
To add more jobs to the workflow, edit .github/workflows/build.yml directly by adding new jobs under the existing jobs: key.
Option 1: Add a linting job
Edit .github/workflows/build.yml and add the following job after the push job:
lint: name: Lint Code runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.12' - name: Install dependencies run: | pip install --upgrade pip pip install flake8 - name: Lint with flake8 run: | flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics || trueNote: Do NOT use cat >> to append to YAML files as it breaks the YAML structure. Instead, edit the file directly with your text editor to add the new job.
Option 2: Add Docker image size check
Create scripts/check-image-size.sh:
mkdir -p scripts cat > scripts/check-image-size.sh << 'EOF'#!/bin/bash IMAGE=$1MAX_SIZE_MB=${2:-500} SIZE_BYTES=$(docker image inspect "$IMAGE" --format='{{.Size}}')SIZE_MB=$((SIZE_BYTES / 1024 / 1024)) echo "Image size: ${SIZE_MB}MB (max: ${MAX_SIZE_MB}MB)" if [ $SIZE_MB -gt $MAX_SIZE_MB ]; then echo "❌ Image exceeds size limit!" exit 1else echo "✅ Image size is acceptable" exit 0fiEOF chmod +x scripts/check-image-size.shStep 8: View Security Scan Results
The workflow runs Trivy vulnerability scanner automatically. To see results, go to your GitHub repository Security tab and click Code scanning alerts. Review any detected vulnerabilities listed there.
For this lab, CleanStart images are minimal, so vulnerabilities should be minimal.
Step 9: Understand Workflow Concepts
Create a reference document:
cat > WORKFLOW_REFERENCE.md << 'EOF'# GitHub Actions Workflow Reference ## Workflow Triggers- `push`: Runs on push to specified branches- `pull_request`: Runs on PR creation/updates- `schedule`: Runs on a cron schedule- `manual` (workflow_dispatch): Run manually from UI ## JobsEach job runs on a separate runner (e.g., ubuntu-latest) ### Job Dependencies- `needs: [build, test]` — Run only after specified jobs complete ## StepsSequential tasks within a job ### Key Actions- `actions/checkout@v4` — Clone repository- `docker/build-push-action@v5` — Build and push Docker images- `actions/upload-artifact@v3` — Store files between jobs- `actions/download-artifact@v3` — Retrieve stored files- `aquasecurity/trivy-action@master` — Scan for vulnerabilities ## Contexts- `${{ github.sha }}` — Commit SHA- `${{ github.ref }}` — Branch/tag reference- `${{ github.repository }}` — Owner/repo- `${{ secrets.GITHUB_TOKEN }}` — Automatic authentication token ## Conditions- `if: success()` — Run only if previous step succeeded- `if: always()` — Run regardless of previous step- `if: failure()` — Run only if previous step failedEOF cat WORKFLOW_REFERENCE.mdStep 10: Add Status Badge to README
Create a README with workflow badge:
cat > README.md << 'EOF'# Lab 06: CI/CD Pipeline  CleanStart application with automated CI/CD pipeline. ## Features - ✅ Automated Docker image building- ✅ Automated testing- ✅ Vulnerability scanning- ✅ Automatic registry push on main branch ## Workflow 1. **Build** → Builds Docker image2. **Test** → Runs health checks and integration tests3. **Scan** → Runs Trivy vulnerability scanner4. **Push** → Pushes to registry (main branch only) ## Local Testing ```bashdocker build -t lab-06-app:latest .docker run -p 8000:8000 lab-06-app:latestcurl http://localhost:8000/healthCI/CD Status
View workflow status in the Actions tab. EOF
git add README.md git commit -m "Add workflow badge and documentation" git push origin main
--- ## Verification Checklist Confirm all of the following to ensure successful completion. GitHub repository is created and cloned locally. `app/main.py` application is created with HTTP endpoints. `app/test.py` test script is created to verify functionality. `Dockerfile` is created using CleanStart base image. `.github/workflows/build.yml` is created with all required jobs. All files are committed and pushed to GitHub. Workflow executes successfully when you push or create a PR. **Build** job completes successfully and creates the image. **Test** job shows "✅ All tests passed!" in output. **Scan** job runs Trivy scanner to check for vulnerabilities. **Push** job only runs on main branch (not on feature branches). GitHub Actions tab shows successful workflow run. `README.md` with workflow badge is created and visible. `WORKFLOW_REFERENCE.md` with documentation is created. If all items are checked, you have successfully completed Lab 06. --- ## What You Learned Through this lab, you learned about **GitHub Actions**: the workflow automation platform. You learned about **Triggers**: how push, pull_request, manual, and scheduled events initiate workflows. You learned about **Jobs**: how parallel and sequential task execution works. You learned about **Artifacts**: how files are passed between jobs. You learned about **Secrets**: secure storage for credentials. You learned about **Matrix Builds**: testing on multiple configurations simultaneously. You learned about **Status Checks**: enforcing quality gates on pull requests. You learned about **Docker Build Action**: efficient multi-stage builds. You learned about **Vulnerability Scanning**: using Trivy for container security. You learned about **Registry Interaction**: authenticating and pushing images. --- ## Cleanup Delete the test branch: ```bashgit branch -d feature/test-cigit push origin --delete feature/test-ciDelete the GitHub repository (optional): Go to repository Settings → Danger Zone → Delete repository
Next Lab
Proceed to Lab 07: Image Customization to learn how to extend CleanStart images with custom packages and configurations.
Real-World Best Practices
Following these practices ensures production-ready CI/CD pipelines. Require passing CI/CD checks before merging to main. Fail the build on high and critical vulnerabilities. Generate SBOMs and attestations in CI/CD. Sign images with cosign for cryptographic verification. Tag images with semantic versioning for clarity. Cache layers to speed up builds significantly. Run security scans on all PR builds before merge. Store artifacts and logs for audit trail and debugging. Use different registries for dev, staging, and production. Implement approval workflows for production deployments.
Estimated Time: 45 minutes | Hands-on: ~35 minutes | Reading: ~10 minutes
