#2559·quartz

[v5] `quartz plugin install` can never restore a plugin pinned to a non-tip commit (shallow fetch misses the pinned SHA, error swallowed)

Author: yuyingziCreated Sep 17, 2026Updated Sep 17, 2026

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 update

The 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.

  1. git clone --depth 1 --branch v5 https://github.com/jackyzha0/quartz.git && cd quartz && npm ci
  2. Add a config + lockfile pinning a plugin to a commit that is not the current tip — quartz.config.yaml:
yaml
configuration:
  pageTitle: repro
  baseUrl: example.com

plugins:
  - source: github:quartz-community/alias-redirects
    enabled: true

quartz.lock.json:

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"
    }
  }
}
  1. Clone the plugin at its current tip, so its HEAD differs from the pinned commit (this is what any previous --latest install leaves behind):
bash
git clone --depth 1 https://github.com/quartz-community/alias-redirects.git .quartz/plugins/alias-redirects
# HEAD = 96411dc, lockfile pin = 73a98dd
  1. npx quartz plugin install

Observed:

→ Installing plugins from lockfile...
  → alias-redirects: updating to 73a98dd...
  ✗ alias-redirects: failed to update

⚠ Installed 0 plugin(s), 1 failed

Running 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:

javascript
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

diff
-          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 dist

Desktop

  • Quartz Version: v5.0.0 (v5 @ 3dff48b)
  • node v22.23.1 / v25.7.0 (same on both)
  • npm 10.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.