Welcome to Chapter 18! Up to this point, we’ve built a powerful, flexible, and efficient Static Site Generator (SSG) in Rust. We’ve handled everything from content parsing and templating to component hydration and incremental builds. However, building the site locally is only half the battle. To truly make our SSG production-ready, we need a robust system for automatically building and deploying our static assets whenever content changes or new features are introduced.

In this chapter, we will focus on the crucial aspects of deployment and Continuous Integration/Continuous Deployment (CI/CD). We’ll explore various static hosting options suitable for our SSG’s output and then design and implement a CI/CD pipeline using GitHub Actions. This pipeline will automate the entire process: fetching the latest code, building our Rust SSG, running tests, generating the static website, and finally deploying it to a chosen hosting provider. By the end of this chapter, you will have a fully automated system that takes your SSG content from source control to a live website with minimal manual intervention, ensuring rapid updates and consistent deployments.

Planning & Design

Before diving into the implementation, let’s outline our deployment strategy and CI/CD workflow.

1. Deployment Targets for Static Sites

Our Rust SSG produces a directory full of static HTML, CSS, JavaScript, and asset files. This makes it ideal for deployment on various static hosting services. Here are some popular options and their characteristics:

  • GitHub Pages: Excellent for projects hosted on GitHub. Free, simple to set up, and integrates seamlessly with GitHub Actions. It’s often sufficient for blogs, documentation, and personal websites.
  • Netlify: A popular choice offering a generous free tier, global CDN, custom domains, HTTPS, and advanced features like serverless functions and form handling. It automatically detects SSGs and provides a smooth deployment experience.
  • Vercel: Similar to Netlify, Vercel provides a fast, global CDN, automatic deployments, and a focus on developer experience. It’s particularly popular for React/Next.js and other JavaScript frameworks but works equally well for any static site.
  • AWS S3 + CloudFront: For more control and enterprise-grade scalability, deploying to an S3 bucket and serving it through CloudFront (AWS’s CDN) is a powerful option. It requires more setup but offers maximum flexibility and integration with other AWS services.
  • Google Cloud Storage + Cloud CDN: Google Cloud’s equivalent to S3 + CloudFront, offering similar benefits and control.

For this chapter, we will focus on GitHub Pages due to its tight integration with GitHub Actions and its simplicity for demonstrating automated deployment. The principles learned, however, will be easily transferable to other platforms.

2. CI/CD Workflow Design with GitHub Actions

Our CI/CD pipeline needs to perform a series of steps automatically whenever we push changes to our main branch. Here’s the proposed workflow:

  1. Trigger: The workflow should start on push events to the main branch.
  2. Environment Setup: Set up a Rust environment on the runner.
  3. Build SSG: Compile our Rust SSG application.
  4. Run Tests: Execute unit and integration tests for our SSG. This ensures that changes haven’t introduced regressions.
  5. Generate Site: Run the compiled SSG executable to generate the static public directory.
  6. Deploy: Publish the contents of the public directory to GitHub Pages.

Here’s a visual representation of our CI/CD pipeline using a Mermaid diagram:

flowchart TD A[Push to main branch] --> B{GitHub Actions Workflow Triggered}; subgraph CI_Pipeline["Continuous Integration Pipeline"] B --> C[Setup Rust Environment]; C --> D[Cache Rust Dependencies]; D --> E[Build SSG Executable]; E --> F[Run Unit and Integration Tests]; F -- "All Tests Pass" --> G[Run SSG to Generate Site]; F -- "Tests Fail" --> H[Notify Failure and Exit]; end subgraph CD_Pipeline["Continuous Deployment Pipeline"] G --> I[Deploy to GitHub Pages]; I -- "Successful Deployment" --> J[Website Live on GitHub Pages]; I -- "Deployment Fails" --> K[Notify Failure and Exit]; end H --> L[End Workflow]; K --> L; J --> L;

Step-by-Step Implementation

We’ll implement this workflow using GitHub Actions.

a) Setup/Configuration

First, ensure your project is a Git repository hosted on GitHub. If not, initialize Git and push your code to a new GitHub repository.

We need to create a new YAML file in your project’s .github/workflows/ directory. This is where GitHub Actions workflows are defined.

File: .github/workflows/deploy.yml

# .github/workflows/deploy.yml
name: Deploy Static Site

on:
  push:
    branches:
      - main # Trigger on pushes to the 'main' branch

env:
  CARGO_TERM_COLOR: always # Always show colored output from Cargo

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest # Use the latest Ubuntu runner

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4 # Action to checkout your repository code

      - name: Setup Rust toolchain
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable # Use the stable Rust toolchain
          profile: minimal
          override: true

      - name: Cache Cargo dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-
            
      - name: Build SSG
        run: cargo build --release # Build our SSG in release mode for performance

      - name: Run tests
        run: cargo test --verbose # Run all tests defined in our SSG project

      - name: Generate static site
        run: ./target/release/your_ssg_name build # Execute our SSG to generate the static files
        # IMPORTANT: Replace 'your_ssg_name' with the actual binary name of your SSG
        # For example, if your Cargo.toml has `name = "my_ssg"`, then it's `my_ssg`.
        # Ensure your SSG's `build` command outputs to a `public` directory.

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        if: github.ref == 'refs/heads/main' # Only deploy if the push is to the main branch
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }} # Automatically provided by GitHub Actions
          publish_dir: ./public # The directory containing your generated static site
          cname: your-domain.com # OPTIONAL: If you're using a custom domain
          # Replace 'your-domain.com' with your actual custom domain, or remove this line.

Explanation of the deploy.yml file:

  • name: A descriptive name for your workflow, which will appear in the GitHub Actions UI.
  • on: Defines when the workflow should run. Here, it runs on push events to the main branch.
  • env: Sets environment variables available to all jobs in the workflow. CARGO_TERM_COLOR: always ensures Cargo output is colored, making logs easier to read.
  • jobs: Workflows are composed of one or more jobs. We have a single job named build_and_deploy.
    • runs-on: Specifies the operating system for the runner. ubuntu-latest is a common choice.
    • steps: A sequence of tasks to be executed in the job.
      • Checkout repository: Uses actions/checkout@v4 to clone your repository onto the runner.
      • Setup Rust toolchain: Uses actions-rs/toolchain@v1 to install the Rust compiler and Cargo. We specify stable for the toolchain.
      • Cache Cargo dependencies: This is a crucial optimization. It caches ~/.cargo/registry, ~/.cargo/git, and the target directory. This significantly speeds up subsequent builds by reusing downloaded crates and compiled artifacts. The key uses a hash of Cargo.lock to bust the cache if dependencies change.
      • Build SSG: Runs cargo build --release. The --release flag compiles your SSG with optimizations, making it faster.
      • Run tests: Executes cargo test --verbose to run all your project’s tests. This is a critical CI step to ensure code quality.
      • Generate static site: This step executes your compiled SSG binary. You MUST replace your_ssg_name with the actual name of the executable your Cargo.toml file defines (e.g., if name = "my_ssg" in Cargo.toml, the binary will be my_ssg). We assume your build command generates files into a public directory, which is a common convention for SSGs.
      • Deploy to GitHub Pages: Uses peaceiris/actions-gh-pages@v4, a popular action for deploying to GitHub Pages.
        • if: github.ref == 'refs/heads/main': Ensures deployment only happens when pushing to the main branch.
        • github_token: ${{ secrets.GITHUB_TOKEN }}: This is a special token automatically provided by GitHub Actions with permissions to deploy to GitHub Pages. You don’t need to create it.
        • publish_dir: ./public: Tells the action which directory contains the static files to deploy.
        • cname: (Optional) If you’re using a custom domain for your GitHub Pages site, specify it here. Otherwise, remove this line.

b) Core Implementation

The core implementation for this chapter is the deploy.yml file itself. We’ve just walked through its creation and explanation.

To make this functional, you need to ensure:

  1. Your SSG’s build command: It must be accessible as ./target/release/your_ssg_name build and produce its output in a directory named public (or whatever you specify for publish_dir).

    • Example from previous chapters (simplified):
      // src/main.rs (simplified for demonstration, assuming you have a `build` function)
      // ... (your existing SSG code) ...
      
      fn main() -> Result<(), Box<dyn std::error::Error>> {
          let args: Vec<String> = std::env::args().collect();
      
          if args.len() > 1 && args[1] == "build" {
              // Ensure the output directory is 'public'
              let output_dir = PathBuf::from("public");
              if output_dir.exists() {
                  std::fs::remove_dir_all(&output_dir)?;
              }
              std::fs::create_dir_all(&output_dir)?;
      
              println!("Building site into: {}", output_dir.display());
              // Your existing build logic here, e.g.,
              // let content_tree = read_content_directory("content")?;
              // let templates = load_templates("templates")?;
              // let rendered_pages = render_content(&content_tree, &templates)?;
              // write_pages_to_disk(&rendered_pages, &output_dir)?;
              // copy_static_assets("static", &output_dir)?;
      
              println!("Site built successfully!");
              Ok(())
          } else {
              eprintln!("Usage: {} build", args[0]);
              Err("Invalid command".into())
          }
      }
      
    • Verify your Cargo.toml name field:
      # Cargo.toml
      [package]
      name = "my_rust_ssg" # This is 'your_ssg_name'
      version = "0.1.0"
      edition = "2021"
      
      # ... other dependencies ...
      
      In this case, the command in the workflow would be ./target/release/my_rust_ssg build.
  2. GitHub Pages Setup:

    • Go to your GitHub repository settings.
    • Navigate to the “Pages” section (usually under “Code and automation”).
    • Under “Build and deployment”, select “GitHub Actions” as the source. This tells GitHub Pages to expect deployments from your workflow.

c) Testing This Component

To test the CI/CD pipeline:

  1. Commit and Push: Commit the deploy.yml file to your main branch.
    git add .github/workflows/deploy.yml
    git commit -m "feat: Add GitHub Actions for CI/CD and deployment"
    git push origin main
    
  2. Monitor Workflow:
    • Go to your GitHub repository.
    • Click on the “Actions” tab.
    • You should see your “Deploy Static Site” workflow running. Click on it to view the real-time logs for each step.
  3. Verify Deployment:
    • Once the workflow completes successfully, go back to your repository settings, then “Pages”.
    • GitHub will provide a URL for your deployed site (e.g., https://your-username.github.io/your-repo-name/).
    • Open this URL in your browser to verify that your static site is live and correctly rendered.

Production Considerations

While GitHub Pages is convenient, production deployments often require more robust considerations:

  1. Environment Variables and Secrets:
    • If your SSG fetches data from external APIs during the build process (e.g., a headless CMS), you might need API keys. Never hardcode these in your workflow file or repository.
    • Use GitHub Secrets for sensitive information. You can define secrets in your repository settings (Settings -> Secrets and variables -> Actions).
    • Access them in your workflow like ${{ secrets.API_KEY }}.
    • Example (if your SSG needed an API key during build):
      # ... inside your 'Generate static site' step ...
      env:
        MY_API_KEY: ${{ secrets.MY_API_KEY }}
      run: ./target/release/your_ssg_name build --api-key $MY_API_KEY
      
  2. Caching: We’ve already implemented Cargo dependency caching. For very large projects, you might also consider caching the public directory itself if only minor changes occur, though for static sites, regenerating the whole thing is often fast enough.
  3. Rollbacks:
    • GitHub Pages keeps a history of deployments. If a deployment is bad, you can revert to a previous successful deployment directly from the GitHub Pages settings UI.
    • For other hosts like Netlify/Vercel, rollbacks are often a built-in feature.
    • For S3/CloudFront, you’d typically manage versions of your S3 bucket content.
  4. Monitoring and Alerting:
    • For critical production sites, you’d want external monitoring (e.g., UptimeRobot, New Relic, Datadog) to check site availability and performance.
    • Integrate alerts (email, Slack, PagerDuty) if the site goes down or experiences errors.
  5. Build Time Optimization:
    • Incremental Builds: Our SSG already supports incremental builds. Ensure your CI/CD pipeline leverages this by passing the appropriate flags to your SSG’s build command (e.g., your_ssg_name build --incremental). This will rely on your SSG’s internal caching mechanisms.
    • sccache: A cacher for Rust compilation. You can integrate sccache into your GitHub Actions workflow for even faster Rust compilation times, especially for clean builds or when the Cargo cache misses.
      # ... after Setup Rust toolchain ...
      - name: Install sccache
        uses: mozilla-actions/sccache-action@v3
      
      - name: Build SSG with sccache
        run: cargo build --release
        env:
          RUSTC_WRAPPER: sccache # Tell Cargo to use sccache
      
  6. CDN Configuration: For maximum performance, ensure your static host uses a Content Delivery Network (CDN) and that caching headers are correctly set for optimal browser caching. GitHub Pages provides a CDN automatically.

Code Review Checkpoint

At this stage, you should have a deploy.yml file in your .github/workflows/ directory. This file defines a GitHub Actions workflow that:

  • Triggers on pushes to the main branch.
  • Sets up the Rust environment and caches dependencies.
  • Builds your SSG executable in release mode.
  • Runs all your SSG’s tests.
  • Executes your SSG to generate the static website into the public directory.
  • Deploys the public directory to GitHub Pages.

This significantly streamlines the process of getting your SSG-generated content live, making content updates and feature releases much more efficient and reliable.

Common Issues & Solutions

  1. Workflow Fails During Rust Build/Test:
    • Issue: cargo build --release or cargo test fails.
    • Solution: Examine the workflow logs carefully. GitHub Actions provides detailed output. Look for Rust compiler errors, test failures, or missing dependencies. Often, it’s a syntax error, a failed test, or a missing use statement. Ensure your code compiles and tests pass locally before pushing.
    • Prevention: Always run cargo build --release and cargo test locally before committing and pushing.
  2. SSG Execution Fails or Output Directory is Empty:
    • Issue: The Generate static site step completes, but the public directory is empty, or the SSG binary cannot be found/executed.
    • Solution:
      • Binary Name: Double-check that ./target/release/your_ssg_name exactly matches the name field in your Cargo.toml.
      • Output Path: Verify that your SSG’s build command actually creates files in the ./public directory. If it creates them elsewhere (e.g., ./dist), update publish_dir in the peaceiris/actions-gh-pages step.
      • Permissions: Ensure your SSG has the necessary permissions to create/write to the output directory. This is usually not an issue on GitHub Actions runners, but good to check.
    • Debugging: Add ls -la target/release and ls -la public commands in your workflow after the SSG execution step to inspect the file system.
  3. Deployment to GitHub Pages Fails (Permissions/Access):
    • Issue: The Deploy to GitHub Pages step fails with permission errors.
    • Solution:
      • GitHub Token: Ensure github_token: ${{ secrets.GITHUB_TOKEN }} is correctly specified. This token is usually sufficient.
      • GitHub Pages Source: Go to your repository settings -> Pages. Under “Build and deployment”, ensure “GitHub Actions” is selected as the source. This grants the workflow the necessary permissions.
      • Branch Protection: If you have branch protection rules on main, ensure the GitHub Actions bot has permissions to push to the gh-pages branch (which peaceiris/actions-gh-pages uses internally).
    • Debugging: Check the logs from the peaceiris/actions-gh-pages step for specific error messages.

Testing & Verification

After implementing the CI/CD workflow:

  1. Trigger a new build: Make a minor change to a content file (e.g., content/index.md), commit, and push to main.
  2. Monitor Actions: Go to your repository’s “Actions” tab and watch the workflow progress.
  3. Verify Success: Ensure all steps in the build_and_deploy job complete successfully (green checkmarks).
  4. Check Deployed Site: Visit your GitHub Pages URL (e.g., https://your-username.github.io/your-repo-name/). Verify that the changes you made to the content file are reflected on the live site.
  5. Test a failure: (Optional, for robustness) Introduce a deliberate error in your SSG code (e.g., a compile error) and push. Observe the workflow failing and the appropriate error messages in the logs. Then revert the change and push again.

This systematic testing ensures your automated deployment pipeline is robust and reliable.

Summary & Next Steps

In this chapter, we successfully established a robust CI/CD pipeline for our Rust SSG using GitHub Actions. We configured the workflow to automatically build, test, and deploy our static website to GitHub Pages upon every push to the main branch. We covered essential production considerations like caching, secrets management, and build time optimizations, ensuring our SSG is ready for real-world use. This automation significantly reduces manual effort, speeds up content delivery, and improves the overall reliability of our content platform.

With our SSG now fully capable of automated deployment, we have a complete, production-ready system. In the next chapters, we will leverage this foundation to build out real-world example projects. These projects will demonstrate the power and flexibility of our Rust SSG, applying all the concepts and features we’ve developed throughout this guide.