When I bootstrap a new machine with chezmoi, I usually start by cloning my dotfiles repo from GitHub over HTTPS:
|
1 2 |
chezmoi init https://github.com/username/dotfiles.git |
That works well for the very first setup, because my SSH keys are not installed yet.
After the first chezmoi apply, though, my SSH keys are finally in place, and I want the dotfiles repository itself to switch from HTTPS to SSH automatically.
This sounds simple, but there is a small gotcha.
The goal
I wanted chezmoi apply to do this at the end of the first run:
- detect that the repo remote is using HTTPS
- convert it to the SSH form
- update
originautomatically
For example:
- from
https://github.com/username/dotfiles.git - to
git@github.com:username/dotfiles.git
My first attempt
My first version used a run_once_after_... script that called chezmoi source-path to find the source repo:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#!/bin/sh set -eu repo_dir="$(chezmoi source-path)" cd "$repo_dir" url="$(git remote get-url origin 2>/dev/null || true)" case "$url" in https://github.com/*) ssh_url="$(printf '%s\n' "$url" \ | sed -E 's#^https://github\.com/([^/]+)/(.+)$#git@github.com:\1/\2#')" git remote set-url origin "$ssh_url" ;; esac |
But when I ran chezmoi apply, I got this:
|
1 2 3 |
chezmoi: timeout obtaining persistent state lock, is another instance of chezmoi running? chezmoi: .chezmoiscripts/switch-origin-to-ssh.sh: exit status 1 |
Why this fails
The problem is that the script is being run by chezmoi apply.
So if the script itself runs another chezmoi command, the second process tries to acquire the same persistent state lock that the original chezmoi apply already holds.
In other words:
chezmoi applystarts- it runs your script
- your script runs
chezmoi source-path - the nested
chezmoiprocess waits on the lock - eventually it times out
The fix
The solution is to avoid calling chezmoi inside the script.
Instead, make the script a template and use {{ .chezmoi.sourceDir }} to inject the source directory path when chezmoi renders the script.
Here is the working version.
Working script
Create this file:
|
1 2 |
.chezmoiscripts/run_once_after_switch-origin-to-ssh.sh.tmpl |
Contents:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#!/bin/sh set -eu repo_dir='{{ .chezmoi.sourceDir }}' cd "$repo_dir" url="$(git remote get-url origin 2>/dev/null || true)" case "$url" in https://github.com/*) ssh_url="$(printf '%s\n' "$url" \ | sed -E 's#^https://github\.com/([^/]+)/(.+)$#git@github.com:\1/\2#')" git remote set-url origin "$ssh_url" echo "Switched origin to $ssh_url" ;; *) echo "origin already uses SSH or is not a GitHub HTTPS URL: $url" ;; esac |
Why this works
There are two important details here.
1. The script is a template
The .tmpl suffix tells chezmoi to render the file before executing it.
That means this line:
|
1 2 |
repo_dir='{{ .chezmoi.sourceDir }}' |
gets replaced with the actual source directory path ahead of time.
No extra chezmoi process is needed.
2. The script runs after “apply”, and only once
Because the file name starts with run_once_after_, chezmoi runs it after the apply phase, and only once per machine unless the script changes.
That makes it a good fit for first-boot setup tasks like this one.
A note about the working directory
One subtle point: chezmoi scripts do not run in the source repo by default.
So even though we know the source directory, we still need to explicitly run:
|
1 2 |
cd "$repo_dir" |
before calling git remote get-url or git remote set-url.
Resetting the script state
If you need to test the script again after it has already run successfully, you can clear chezmoi’s script state:
|
1 2 |
chezmoi state delete-bucket --bucket=scriptState |
That lets run_once_ scripts execute again.
Final thoughts
This ended up being a neat example of a general chezmoi rule:
If a script is already running inside
chezmoi apply, avoid launchingchezmoiagain from that script.
When you need chezmoi-specific information in a script, templating is often the better solution.
For my bootstrap flow, this gives me the best of both worlds:
- clone over HTTPS when no SSH key exists yet
- install SSH keys during the first apply
- automatically switch the dotfiles repo to SSH at the end
Exactly what I wanted.
If you use a similar bootstrap flow, hopefully, this saves you from the persistent state lock timeout.