[v5] `quartz plugin install` can never restore a plugin pinned to a non-tip commit (shallow fetch misses the pinned SHA, error swallowed)
Describe the bug
In v5, npx quartz plugin install cannot re-checkout any plugin whose lockfile-pinned commit is not the tip of the remote default branch. Each affected plugin prints:
→ <name>: updating to <sha>...
✗ <name>: failed to updateThe update path does git fetch --depth 1 origin and then git reset --hard <entry.commit>. On a shallow clone the fetch only brings the remote HEAD tip, so the pinned commit is not in the object database and the reset dies with fatal: Could not parse object '<sha>'. The catch in the runParallel callback swallows that, so the only user-visible output is the bare "failed to update".
On a site with 29 git plugins, a single npx quartz plugin install hit this for 28 of them and ended with ⚠ Installed 17 plugin(s), 28 failed.
Impact: the plugin working tree silently stays at whatever commit it was on, instead of the one recorded in quartz.lock.json — so local installs drift from the lockfile, and the failure is undiagnosable from the output.
To Reproduce
Quartz v5 @ 3dff48b, node v22.23.1, npm 10.9.8, macOS arm64.
git clone --depth 1 --branch v5 https://github.com/jackyzha0/quartz.git && cd quartz && npm ci- Add a config + lockfile pinning a plugin to a commit that is not the current tip —
quartz.config.yaml:
configuration:
pageTitle: repro
baseUrl: example.com
plugins:
- source: github:quartz-community/alias-redirects
enabled: truequartz.lock.json:
{
"version": "1.0.0",
"plugins": {
"alias-redirects": {
"source": "github:quartz-community/alias-redirects",
"resolved": "https://github.com/quartz-community/alias-redirects.git",
"commit": "73a98dda7e4f55239310833299d91daf8611349f",
"installedAt": "2026-06-11T07:16:49.577Z"
}
}
}- Clone the plugin at its current tip, so its HEAD differs from the pinned commit (this is what any previous
--latestinstall leaves behind):
git clone --depth 1 https://github.com/quartz-community/alias-redirects.git .quartz/plugins/alias-redirects
# HEAD = 96411dc, lockfile pin = 73a98ddnpx quartz plugin install
Observed:
→ Installing plugins from lockfile...
→ alias-redirects: updating to 73a98dd...
✗ alias-redirects: failed to update
⚠ Installed 0 plugin(s), 1 failedRunning the two git commands from the update path by hand in .quartz/plugins/alias-redirects shows the real error:
$ git fetch --depth 1 origin
$ git reset --hard 73a98dda7e4f55239310833299d91daf8611349f
fatal: Could not parse object '73a98dda7e4f55239310833299d91daf8611349f'.Expected behavior
The pinned commit is fetched and checked out, and the plugin is reported as installed/updated. If that genuinely fails, the git error should be printed instead of a bare "failed to update".
Screenshots and Source
quartz/cli/plugin-git-handlers.js:1093-1124 on v5 @ 3dff48b:
if (action === "update") {
console.log(styleText("cyan", ` → ${name}: updating to ${entry.commit.slice(0, 7)}...`))
const fetchRef = entry.ref ? ` ${entry.ref}` : ""
await execAsync(`git fetch --depth 1 origin${fetchRef}`, { cwd: pluginDir }) // <- no SHA
await execAsync(`git reset --hard ${entry.commit}`, { cwd: pluginDir }) // <- SHA not present
...
} else {
...
await execAsync(`git fetch --depth 1 origin ${entry.commit}`, { cwd: pluginDir }) // <- clone path does it right
await execAsync(`git checkout ${entry.commit}`, { cwd: pluginDir })The clone branch eight lines below fetches the specific commit; the update branch fetches the default branch instead.
Suggested fix
- const fetchRef = entry.ref ? ` ${entry.ref}` : ""
+ const fetchRef = entry.ref ? ` ${entry.ref}` : ` ${entry.commit}`
await execAsync(`git fetch --depth 1 origin${fetchRef}`, { cwd: pluginDir })
await execAsync(`git reset --hard ${entry.commit}`, { cwd: pluginDir })and include the stderr in the catch so a real failure is visible.
Verified by hand on a shallow clone that did not have the commit:
$ git fetch --depth 1 origin 73a98dda7e4f55239310833299d91daf8611349f
* branch 73a98dda7e4f55239310833299d91daf8611349f -> FETCH_HEAD
$ git reset --hard 73a98dda7e4f55239310833299d91daf8611349f
HEAD is now at 73a98dd build: bundle dependencies and ship pre-built distDesktop
- Quartz Version: v5.0.0 (
v5@3dff48b) nodev22.23.1 / v25.7.0 (same on both)npm10.9.8- OS: macOS 15 (arm64)
Additional context
Not a build blocker — regeneratePluginIndex() still runs afterwards, .quartz/plugins/index.ts is written and npx quartz build succeeds. It only means the on-disk plugin tree can differ from quartz.lock.json. Might be worth a git cat-file -e <sha> guard before the reset, so the update path can fall back to fetching the SHA explicitly.
Source: jackyzha0/quartz