Skip to content

Cookiecutter with Cruft for Platform Engineering

Posted on:February 27, 2025

Cookiecutter with Cruft for Platform Engineering

Overview

What is Cookiecutter?

Cookiecutter is a tool used to create projects from project templates. I’ve explored cookiecutter in a previous article, Cookiecutter for Platform Engineering. We wont go into the details of cookiecutter in this article, but we will be using it to create a project template.

Why Use Cookiecutter with Cruft?

While cookiecutter is great for creating projects from templates, it doesn’t provide a way to update existing projects with changes from the template. This is where Cruft comes in. Cruft is a tool that helps you manage and maintain project generated from cookiecutter templates. It can detect changes in the template and apply them to the project in a familiar git merge fashion.

What We’ll Cover

In this article, we’ll learn the basics of creating a project from a cookiecutter template using Cruft and demonstrate how to update the project with changes from the template, manually with Cruft, and automatically with GitHub Actions.

Prerequisites

Before we get started, make sure you have the following installed:

I recommend using asdf to manage your local development environment. You can install Python 3 using asdf with the following command:

asdf plugin add python
asdf install python 3.13.2
asdf global python 3.13.2

Installing Cookiecutter and Cruft

Follow the documentation to install Cookiecutter and Cruft.

Using Cruft

To create a project from a cookiecutter template using Cruft, make sure you in a directory where you want a new project directory to be created in. Then run the following:

cruft create gh:faradayfan/basic-cookiecutter-template
# [1/4] project_name (My App):
# [2/4] project_slug (my-app):
# [3/4] project_description (My App is a simple app.):
# [4/4] author (Your Name):

I just used the default values for the prompts, but you can enter your own values. This will create a new project directory with the files and directories defined in the cookiecutter template.

My template created a Dockerfile with the following content:

FROM nginx:1.26.3

But I noticed that there is a new version of the nginx image available. I can update the template with the new image version and apply the changes to the project with Cruft. Once the template is updated, I can run the following:

cruft check
# FAILURE: Project's cruft is out of date! Run `cruft update` to clean this mess up.

cruft update
# Respond with "s" to intentionally skip the update while marking your project as up-to-date or respond with "v" to view the changes that will be applied.
# Apply diff and update? (y, n, s, v) [y]: y
# Good work! Project's cruft has been updated and is as clean as possible!

Notice the files changes:

diff --git a/.cruft.json b/.cruft.json
index bd9c3cd..58147f6 100644
--- a/.cruft.json
+++ b/.cruft.json
@@ -1,6 +1,6 @@
 {
   "template": "https://github.com/faradayfan/basic-cookiecutter-template.git",
-  "commit": "0ddc966ea500569f1f515b8fba94c06567238056",
+  "commit": "5a7322cc1a53485c371e043c8b1ddda164885c55",
   "checkout": null,
   "context": {
     "cookiecutter": {
@@ -9,7 +9,7 @@
       "project_description": "My App is a simple app.",
       "author": "Your Name",
       "_template": "https://github.com/faradayfan/basic-cookiecutter-template.git",
-      "_commit": "0ddc966ea500569f1f515b8fba94c06567238056"
+      "_commit": "5a7322cc1a53485c371e043c8b1ddda164885c55"
     }
   },
   "directory": null
diff --git a/Dockerfile b/Dockerfile
index fc2aa20..4f3543c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1 +1 @@
-FROM nginx:1.26.3
+FROM nginx:1.27.4

It updated the Dockerfile with the new image version and updated the .cruft.json file with the new template commit. The project is now up-to-date with the template.

Automating with GitHub Actions

To automate the process of updating the project, we can create a GitHub Actions workflow that runs on a schedule. Cruft provides an example workflow that you can use, but I suspect due to some security changes to run generated tokens, the workflow will fail. Here is a modified version that worked for me.

You will need to create either a “classic” Personal Access Token with the repo scope or a “Fine Grained” Personal Access Token with the following permissions:

Once created, add it as a secret to your repository with the name CRUFT_UPDATE_TOKEN.

name: Update repository with Cruft
on:
  schedule:
    - cron: "0 2 * * 1" # Every Monday at 2am

  workflow_dispatch:

permissions: {}

jobs:
  update:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - add-paths: .
            body: Use this to merge the changes to this repository.
            branch: cruft/update
            commit-message: "chore: accept new Cruft update"
            title: New updates detected with Cruft
          - add-paths: .cruft.json
            body: Use this to reject the changes in this repository.
            branch: cruft/reject
            commit-message: "chore: reject new Cruft update"
            title: Reject new updates detected with Cruft
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.CRUFT_UPDATE_TOKEN }}

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.x"

      - name: Setup GitHub CLI and Git
        run: |
          if [ -z "${{ secrets.CRUFT_UPDATE_TOKEN }}" ]; then
            echo "CRUFT_UPDATE_TOKEN secret is not set, exiting"
            exit 1
          fi
          echo "${{ secrets.CRUFT_UPDATE_TOKEN }}" | gh auth login --with-token
          gh auth setup-git
          git config --global user.email "example@email.com"
          git config --global user.name "your-username"

      - name: Install Cruft
        run: |
          python -m pip install cruft
          python -m cruft

      - name: Check if update is available
        continue-on-error: false
        id: check
        run: |
          CHANGES=0
          if [ -f .cruft.json ]; then
            if ! python -m cruft check; then
              CHANGES=1
            fi
          else
            echo "No .cruft.json file"
          fi

          echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT"

      - name: Run update if available
        if: steps.check.outputs.has_changes == '1'
        run: |
          python -m cruft update --skip-apply-ask --refresh-private-variables
          git restore --staged .

      - name: Create pull request
        if: steps.check.outputs.has_changes == '1'
        run: |
          git checkout -b "${{ matrix.branch }}"
          git add "${{ matrix.add-paths }}"
          git commit -m "${{ matrix.commit-message }}"
          git push origin "${{ matrix.branch }}" --force # Replaces the branch if it already exists
          EXISTING_PR=$(gh pr list --state open --base main --head "${{ matrix.branch }}" --json number --jq '.[0].number')
          if [ -n "$EXISTING_PR" ]; then
            echo "PR already exists: $EXISTING_PR, skipping creation"
          else
            gh pr create --title "${{ matrix.title }}" --body "${{ matrix.body }}" --base main --head "${{ matrix.branch }}"
          fi

When this workflow runs, if a new update is detected, it will create 2 branches: cruft/update and cruft/reject. The cruft/update branch will contain the changes from the template, and the cruft/reject branch will contain the changes to reject the update. You can then review the changes and create a pull request to merge the changes or reject them. Here is a merge request that was created from the workflow in our example project.

This workflow is exactly the kind of thing that should be added to the template. It will help keep all projects that are created from the template up-to-date with the latest changes.

Conclusion

In this article, we learned how to use Cookiecutter with Cruft to create a project template for platform engineering. We covered the basics of creating a project from a cookiecutter template using Cruft and demonstrated how to update the project with changes from the template, manually with Cruft, and automatically with GitHub Actions. This is a powerful combination that can help you manage and maintain project templates and keep projects up-to-date with the latest changes.

References