Skip to content

hk-config consumers pin hk twice; skew breaks Pkl module identity #392

Description

@gtbuchanan

Defaults.pkl imports Config.pkl/Builtins.pkl from a version-pinned hk release, and consumers separately amends their own pinned hk Config.pkl. That's two pins for the same Pkl module moved by two different clocks: renovate raises the consumer's pin to the newest hk release, while hk-config's pin is frozen in whatever the last hk-config release shipped. Whenever hk publishes ahead of tooling, the graph is skewed and Pkl rejects it on module identity.

This broke gtbuchanan/dotfiles#64, where the consumer amended hk 1.54.1 while hk-config@0.2.3 imported 1.54.0:

Module version conflict: Expected value of type `hk.Config#Hook` defined in module
`…hk@1.54.1#/Config.pkl`, but got type `hk.Config#Hook` defined in module `…hk@1.54.0#/Config.pkl`.

Grouping the two bumps into one PR (which the shared Renovate config already does) makes them land together but cannot make them agree — hk-config's pin lives inside a published artifact.

Status: blocked upstream

The end state is right — the pin should exist in exactly one place — but every mechanism for getting there is blocked by hk, not by Pkl. Details below. Blocked rather than rejected: a fix to hk's module resolver makes this implementable immediately.

Rejected: publish packages/hk-config/Config.pkl for consumers to amend

The original proposal here was to add a Config.pkl that amends hk's Config.pkl and holds the sole hk pin, so consumers amend that instead of naming an hk version at all. It was implemented and does not work.

Built hk-config with Config.pkl plus a Builtins.pkl re-export (needed too — builtin steps are typed by the Config.pkl that hk's own Builtins.pkl imports, so importing hk's builtins directly reintroduces the same pin), injected the package into Pkl's cache, and ran four consumers against the same hk-config@0.2.3 cache entry with hk 1.54.1 and Pkl 0.32.1:

Consumer hk.pkl hk validate
amends hk directly, imports the package's Defaults.pkl (status quo) valid
amends the package's Defaults.pkl (a module with no nested package amends) valid
amends the package's Config.pkl (the proposal) IO error reading …\pklr-pkg-0-…\Config.pkl
full proposed usage (Config + Defaults + Builtins) same error

Same package, same cache entry, same shell — only the imported module differs. hk cannot resolve a package module whose own header is amends "package://…". It extracts the package to a temp directory and then fails to read the file back out. Deterministic across repeated runs.

Every one of those consumers evaluates correctly under real pkl eval, including the full facade. This is hk's Pkl evaluation path, not Pkl semantics and not a flaw in the pattern. It also explains the pre-filing verification that suggested this would work: the chained amends does render fully under pkl, and does not under hk.

Rejected: Pkl project dependencies (minimum version selection)

The obvious alternative, and the one Pkl actually designed for this: MVS over project dependencies would unify a consumer on 1.54.1 with a preset on 1.54.0 automatically and make the facade unnecessary. Unavailable for two independent reasons:

  • hk's Pkl evaluation path isn't project-aware. With a valid PklProject and resolved PklProject.deps.json beside it, hk validate fails with Import not found: …\@hk/Builtins.pkl — it resolves @hk/… as a literal relative path. The identical file renders under pkl eval --project-dir.
  • Even with that fixed, MVS still wouldn't unify hk's pins. hk's package URI embeds the version in the release download path, so the resolver keys …/download/v1.54.1/hk@1 and …/download/v1.54.0/hk@1 as two unrelated packages. Resolving a consumer on 1.54.1 alongside a preset on 1.54.0 emits two live hk entries rather than one.

A version-free baseUri upstream fixes the second half. Our own hk-config is already shaped correctly for this (package://github.com/gtbuchanan/tooling/hk-config@0).

Untested

An extends-based facade instead of amends is untested, not ruled out. The offline cache-injection harness stopped honoring freshly injected entries partway through, and a control using amends on the same fresh entry failed identically — so that run says nothing about extends. Confirming it needs a real published prerelease.

Separately, the hk binary tolerates trailing the Pkl module version: hk validate passes with a binary several minors ahead of the module graph, and the drift direction is safe by construction, since renovate keeps the binary at latest and hk-config can only pin an already-published hk. That still holds and is not affected by the above.

Next

  • A gtb verify hk guard comparing the consumer's amends version against the hk-config release's pin, turning a Module version conflict deep in Pkl into a clear "bump hk to X". Doesn't remove the double pin, removes the baffling failure. Actionable now, independent of upstream — tracked separately.
  • Report the nested-package amends failure upstream. jdx/hk has issues disabled, and discussion #1008 — someone hitting this from the other direction, trying to publish a package exporting Config.Step — has been unanswered since June. A minimal deterministic reproducer of a resolver bug is a stronger thing to post than a design question, and it's the blocker for the pattern two people arrived at independently.
  • The Renovate hk group rule stays the mitigation meanwhile; it carries more weight than previously credited.

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedBlocked on an external dependency or upstream fixenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions