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:
- Python 3
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:
- “Read access to metadata”
- “Read and Write access to administration, code, pull requests, and workflows”
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.