Maintaining Wrapper Packages
This guide covers the day-to-day workflows for maintaining a datamitsu wrapper package — keeping tool versions current, verifying integrity across platforms, and automating updates.
Overview
A wrapper package is a datamitsu configuration that bundles a curated set of tools for a specific use case (e.g., a company's standard linting stack). As a wrapper maintainer, your responsibilities include:
- Updating tool versions when new releases are published
- Regenerating lock files for reproducible installs
- Verifying SHA-256 hashes across all supported platforms
- Testing that updated tools work correctly
datamitsu provides devtools commands to automate these tasks.
Global-install parity: declaring getBeforeConfigs()
A wrapper package typically ships a bin/datamitsu shim that injects --before-config <shared config> so the shared configuration loads ahead of the project's own. That shim only fires when datamitsu is invoked through it (e.g. via pnpm exec datamitsu). A globally installed datamitsu run inside the same repo never receives the flag — it skips the shared config, the effective configuration diverges, and the user has to hand-edit the command to reproduce the wrapper's behaviour.
getBeforeConfigs() closes that gap. Export it from the project's git-root config to declare the shared config as a local under-layer, so the global binary reproduces the wrapper automatically:
/// <reference path=".datamitsu/datamitsu.config.d.ts" />
function getBeforeConfigs() {
return [{ path: "./node_modules/@myorg/datamitsu-config/datamitsu.config.js" }];
}
globalThis.getBeforeConfigs = getBeforeConfigs;
function getConfig(config) {
// config already includes the shared wrapper config's changes
return { ...config };
}
globalThis.getConfig = getConfig;
globalThis.getMinVersion = () => "0.0.1";
The declared path is inserted into the loading order immediately before the auto config — exact parity with the --before-config flag (init-layer merging, getMinVersion() checks, remote-config resolution all apply identically).
When to use which:
| Situation | Mechanism |
|---|---|
| Wrapper-managed repo, always run via the shim | The shim's --before-config is enough — declaring is optional |
Same repo also run with a global datamitsu | Declare getBeforeConfigs() so both invocations match |
| Debugging — want to run the global binary directly | Declare getBeforeConfigs(); no command rewriting needed |
Precedence (the flag always wins): when --before-config is passed on the CLI, getBeforeConfigs() is not evaluated at all. This is deliberate — when the wrapper shim is in use, the flag already loads the shared config, and skipping the declaration avoids loading it twice. So adding getBeforeConfigs() is safe even for repos that are sometimes run through the shim and sometimes via the global binary.
A few rules to keep in mind:
- Relative paths resolve against the git-root config's directory; absolute paths are used as-is. A missing file is a hard error.
- No hash is required — a local path is in the same trust domain as the root config, not a network download (unlike
getRemoteConfigs(), which mandates a SHA-256 hash). - It is honoured only in the auto-discovered git-root config. A
getBeforeConfigs()declared inside the shared config itself (or any other layer) is ignored — there is no chaining.
See Configuration → Declared Before-Configs for the full reference.
Wrapping a private binary
A Bun proxy can call a managed native executable through an explicit app
binding. Keep the upstream's existing SHA-256-pinned binary declaration and
supported-platform map. The example below assumes your config build supplies
upstreamBinary (that declaration) and proxyLockFile (the generated pnpm
lockfile for your published proxy package, here named
@example/lefthook-proxy). It also retains the existing managed Bun runtime and
its pinned pnpm runtime.
function getConfig(config) {
return {
...config,
apps: {
...config.apps,
"dm-internal-lefthook-upstream": {
binary: upstreamBinary,
},
lefthook: {
bun: {
packageName: "@example/lefthook-proxy",
version: "1.0.0",
binPath: "node_modules/@example/lefthook-proxy/proxy.js",
lockFile: proxyLockFile,
},
dependsOn: ["dm-internal-lefthook-upstream"],
runtimeEnv: {
DATAMITSU_LEFTHOOK_UPSTREAM: "${APP_BIN:dm-internal-lefthook-upstream}",
},
},
},
};
}
globalThis.getConfig = getConfig;
The proxy package's proxy.js can forward the invocation directly:
const child = Bun.spawn([process.env.DATAMITSU_LEFTHOOK_UPSTREAM, ...process.argv.slice(2)], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
process.exit(await child.exited);
The child receives one exact, version-specific executable path. Avoid scanning
${STORE}/.bin/<name> and probing candidate versions; required: true on the
upstream is unnecessary. The dm-internal- prefix is a naming convention, not
an access-control or shim-exclusion setting.
When init selects lefthook, it installs the upstream too. install lefthook,
exec lefthook, and tools using it through check, lint, fix, or LSP all
provision its transitive closure, regardless of the dependency's required or
lazy flags. store seed --apps lefthook includes the dependency's store
subtree and the runtimes needed by the selected apps; automatic seeding uses
the same closure. This selects expected seed content without changing the
bundle's existing coverage/failure policy.
Source-mode activation stays download-free. Its farm records dependency health
paths even for names excluded from the shim farm. If an upstream file is later
removed, invoking the root shim delegates repair to datamitsu install lefthook
and restores the dependency before execution. Neither dependsOn nor
runtimeEnv changes the proxy's install hash. A binding must target a direct
native binary dependency; a runtime-managed dependency needs its full command
and cannot be represented by ${APP_BIN:...}.
Updating Tool Versions
Minimum release age (--min-age)
All pull-* commands refuse to select a release younger than the minimum release age (7 days / 10080 minutes by default). This soak time lets the ecosystem catch typosquats and compromised or broken publishes before you pin them. Every command accepts the shared --min-age <minutes> flag and prints the effective cutoff in its banner:
--min-age -1(default) — use the global effective minimum release age--min-age 0— disable filtering and take the newest release--min-age 43200— custom cutoff (30 days)
# Default 7-day filter
datamitsu devtools pull-releases apps/binaryApps.json --update
# Require 30 days of soak time
datamitsu devtools pull-node apps/nodeApps.json --update --min-age 43200
# Bypass the filter (e.g. to adopt a same-day security release)
datamitsu devtools pull-releases apps/binaryApps.json --update --min-age 0
Set DATAMITSU_MIN_RELEASE_AGE (minutes) to change the default for every command, including the check config lockfile runs on a Go app's resolved modules. When no release is old enough, pull-releases keeps an existing app's current tag (with a warning) but records a failure for a brand-new app; pull-node/pull-uv skip the package with a warning; pull-runtimes hard-errors. See Supply Chain Security → Minimum Release Age for the full behavior table and the registries it covers.
Order and progress
Every pull-* command works through its entries in alphabetical order — apps, packages and runtimes alike, except that pull-runtimes takes pnpm first because the Node and Bun entries name it — and prints a counter with each (=== Processing snyk [12/80] ===, [12/80] snyk 1.0.0 up-to-date), so a long run says how far along it is. The file it writes has the keys of every object sorted, whatever the order they were declared in, so the diff of a pull shows what changed and nothing else. A file written by an older build is rewritten in that order the first time a pull saves it.
Failures, retries and exit codes
A registry pull is how versions and hashes enter a configuration, so an app that is silently skipped is a version that silently stays behind. Every pull-* command therefore:
- retries transient failures — network errors, timeouts, 5xx responses and rate limits that name a short wait — making up to four attempts with backoff, printing each retry with the request, the attempt and the reason; under
--verify-extractionthe asset downloads retry the same way; - keeps going past an app or package that fails, so one bad entry does not cost the run;
- ends with a report naming every failure, the stage it failed at and the error, with a hint when the fix is a setting (a GitHub rate limit → set
GITHUB_TOKEN); - exits with status 1 when anything failed after the retries, so a script or a CI job sees it.
Permanent failures — a 404, a release whose assets carry no digest, a validation error, a rate limit that resets later than two minutes — are not retried, but they are in the report and in the exit code all the same.
Under --verify-extraction, only a fault in the asset itself — a hash mismatch, an archive that does not extract, a file that is not an executable — makes pull-releases fall back to the next-ranked asset for the platform. A download that fails is not the asset's fault, so the platform is not handed to whatever ranks below (an attestation file, or a variant that happens to verify): it fails the app, whose previous entry is kept, and the report names the platform and the asset (verify linux/amd64/glibc).
Every pull-* command that writes its file saves it after each entry that changed, before it starts the next, and replaces the file whole each time. After a failed or interrupted run every entry is therefore either the previous state of one that failed or was not reached, or the complete new state of one that succeeded, never a partial one; rerun the command to pick up the rest. A save that fails stops the run. pull-runtimes pulls pnpm before the runtimes that name it, and does not record a Node or Bun entry whose pnpmRuntime the file does not define: that runtime fails and keeps its previous entry.
Run large pulls with GITHUB_TOKEN set: the unauthenticated GitHub API allows 60 requests an hour, which a registry of a few dozen apps exhausts part-way.
Inspecting the effective runtime config (datamitsu config runtime)
datamitsu config runtime prints the full effective runtime configuration as JSON — the env-resolved view of execution limits, the per-app install timeout, and the minimum release age. Use it to mechanically confirm what the tool will actually run with, including any DATAMITSU_* overrides, before kicking off a pull or an install:
# Inspect the full snapshot
datamitsu config runtime
# Confirm the effective minimum release age (minutes)
datamitsu config runtime | jq .minimumReleaseAgeMinutes # -> 10080
# Confirm an env override took effect
DATAMITSU_MIN_RELEASE_AGE=20160 datamitsu config runtime | jq .minimumReleaseAgeMinutes # -> 20160
# Verify the per-app install timeout (seconds; 0 = disabled)
DATAMITSU_INSTALL_TIMEOUT=1200 datamitsu config runtime | jq .installTimeoutSeconds # -> 1200
The snapshot is introspection-only — it is not injected into the config JS VM. Wrapper config authors who need to branch on the effective minimum release age inside their config.js read the bounded, frozen datamitsuConfigInputs global instead:
function getConfig(config) {
// Only `minimumReleaseAgeMinutes` is exposed today, and the object is frozen.
if (datamitsuConfigInputs.minimumReleaseAgeMinutes === 0) {
// age filtering is disabled — branch accordingly
}
return config;
}
datamitsuConfigInputs is a deliberately minimal allowlist: it carries only the runtime values config evaluation is permitted to depend on (currently just minimumReleaseAgeMinutes). The full config runtime snapshot is never exposed to config JS — that boundary keeps hidden config inputs from leaking into fingerprinting and caching.
Binary Apps: devtools pull-releases
Binary apps are downloaded from GitHub, GitLab, Gitea or Forgejo releases through named source profiles. Use pull-releases to fetch the latest release versions and published SHA-256 digests.
The registry uses sources aliases and per-app source, repository and tag.
There is no schema version or implicit GitHub fallback. See the
release provider reference
for instance URLs, token references, integrity pins and migration from the old
owner/repo registry.
Apply updates:
datamitsu devtools pull-releases apps/binaryApps.json --update
With --update, the command fetches the latest release tags, detects binaries for all platform tuples (Darwin/Linux/Windows/FreeBSD/OpenBSD on amd64/arm64, Linux with glibc/musl), resolves their mandatory SHA-256 digests, fetches the repository description from the source API, and writes the results back to the JSON file.
To keep only specific platforms, add a top-level list to binaryApps.json:
"platforms": ["darwin/arm64", "linux/amd64/musl"]
The list strictly matches supported identifiers and removes unselected existing
binary records before the pull. Those deletions remain saved if the pull fails.
Unavailable selected platforms are reported and omitted when another selected
platform is available. For releases that publish Linux only as musl, include
linux/amd64/musl to retain that record; a musl asset is never relabeled as glibc.
Changing the set or removing the field reruns detection even at the same tag.
See the command reference
for supported values and validation rules.
Verify binary extraction after update:
datamitsu devtools pull-releases apps/binaryApps.json --update --verify-extraction
The --verify-extraction flag additionally downloads each binary (retrying a failed download like any other request), extracts it, and checks that the result is an executable: an ELF, Mach-O or PE file, or a script starting with #!. This catches changed archive structures, renamed binaries inside archives, and a binaryPath that picks a completion script or a man page instead of the binary.
pull-releases learns binaryPath from the entries already in the file. An asset published under the same name as before keeps the binaryPath recorded for it. Otherwise the closest entry lends its layout — the same OS, architecture and libc first, then the same OS and architecture, then the same OS — with the new asset's name substituted into the path component named after the old asset (tombi-cli-1.5.0-x86_64-apple-darwin/tombi becomes tombi-cli-1.5.5-x86_64-apple-darwin/tombi), or else only the version. A layout that names another architecture or libc than the asset is never borrowed. A binaryPath you corrected by hand therefore survives the next release instead of being guessed again.
An asset that names an operating system datamitsu does not run on — illumos, Solaris, NetBSD, DragonFly, Haiku, Android, AIX, Plan 9 — is never recorded for Linux, even when it carries an x86_64 token and no other. On Windows, win64 counts as amd64 and win32 as 32-bit, and a bare win token or a .exe suffix is enough to name the OS.
:::note Releases with mixed asset types
Some tools publish VS Code extensions (.vsix), Linux packages (.deb, .rpm), NuGet packages (.nupkg), Python wheels (.whl), Windows installers (.msi), or macOS installer packages (.pkg) alongside binary archives in the same release. datamitsu automatically excludes these non-executable formats before scoring, so only actual binaries compete for selection. No configuration is needed — the filtering is automatic.
:::
Node Apps (npm): devtools pull-node
Node apps are npm packages managed via pnpm. Use pull-node to check for updates on the npm registry.
Preview available updates:
datamitsu devtools pull-node apps/nodeApps.json --update --dry-run
The --update --dry-run combination fetches the latest versions and shows what would change without writing to the file.
Apply updates:
datamitsu devtools pull-node apps/nodeApps.json --update
This queries the npm registry for each configured package, compares versions, and updates the JSON file with new versions and descriptions.
Regenerate lock files after updating:
After updating node app versions, regenerate their lock files to ensure reproducible installs:
datamitsu config lockfile prettier
datamitsu config lockfile eslint
Each command outputs brotli-compressed lock file content (with a br: prefix) that you paste into the app's lockFile field in your configuration.
UV Apps (Python): devtools pull-uv
UV apps are Python packages installed in isolated environments. Use pull-uv to check for updates on PyPI.
Preview available updates:
datamitsu devtools pull-uv apps/uvApps.json --update --dry-run
Apply updates:
datamitsu devtools pull-uv apps/uvApps.json --update
This queries PyPI for each configured package, compares versions, and updates the JSON file.
Regenerate lock files after updating:
datamitsu config lockfile yamllint
pull-uv ages only the package itself. Its dependencies are resolved here, under the 7-day window uv records in the lock: a regenerated lock carries that window, and an older lock without one keeps installing unchanged. See Release Age of Transitive Dependencies.
Runtimes: devtools pull-runtimes
Runtimes (Bun, Node.js, UV/Python, JVM/Temurin, and Go) need periodic updates too. Use pull-runtimes to fetch the latest runtime versions.
Update all runtimes:
datamitsu devtools pull-runtimes runtimes/runtimes.json --update
The --update flag is required as a safety guard — the command refuses to run without it.
Update a specific runtime only:
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime bun
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime node
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime uv
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime jvm
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime pnpm
Preview changes without writing:
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --dry-run
The command fetches versions from upstream sources:
- Bun: latest eligible release and official archive SHA-256 digests from GitHub
- Node.js: latest LTS version from endoflife.date API
- pnpm: latest pnpm 12 release old enough for the minimum release age (judged by its npm publish date), with per-platform archive SHA-256 digests from the matching GitHub release
- Python: latest stable (non-EOL) version from endoflife.date API
- Java (Temurin): latest major version from Adoptium API, or the previous one while the latest has no build old enough for the minimum release age
It then downloads runtime binaries for all platform tuples, computes SHA-256 hashes, and deduplicates musl entries that are identical to glibc.
Bumping the Bun runtime
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime bun
The updater selects the latest Bun GitHub release allowed by the configured minimum release age and records the official SHA-256 digest for each macOS, Linux glibc, Linux musl, and Windows Bun archive on amd64 and arm64. The Bun entry points at the pnpm runtime with pnpmRuntime: "pnpm".
Bumping the pnpm runtime
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime pnpm
pnpm is a runtime of its own, shared by the Node and Bun runtimes through pnpmRuntime. The updater picks the newest pnpm 12 release whose npm publish date passes the configured minimum release age, then pins the native archive for each macOS, Linux glibc, Linux musl, and Windows platform on amd64 and arm64 with the SHA-256 digests published in the matching pnpm/pnpm GitHub release. A pnpm bump reinstalls Node and Bun apps on their next use without downloading Node or Bun again.
Bumping the Node.js runtime
The Node.js runtime is a direct, hash-pinned archive (like the JVM runtime), so bumping it means refreshing the pinned version and the per-platform hashes in runtimes.json:
datamitsu devtools pull-runtimes runtimes/runtimes.json --update --runtime node
This resolves the latest Node.js LTS, builds the archive URLs for every os/arch/libc combination, and records a SHA-256 hash for each:
- glibc, macOS, and Windows hashes are taken from nodejs.org's clearsigned
SHASUMS256.txt.ascand GPG-verified against the embedded official Node.js release public keys. An invalid or untrusted signature aborts the pull, giving a strong supply-chain anchor. - musl hashes come from unofficial-builds.nodejs.org's unsigned
SHASUMS256.txt(Node publishes no signature for musl builds) and are pinned in git — the same trust model as the officialnode:alpineimage.
After a Bun, Node.js, or pnpm version bump, regenerate the lock files for the affected Bun or Node apps so they resolve against the new runtime, then commit the refreshed runtimes.json:
datamitsu config lockfile eslint
datamitsu config lockfile prettier
Bumping the Go runtime
datamitsu devtools pull-runtimes runtimes.json --update --runtime go
The Go SDK archives and their per-file SHA-256 come from go.dev (https://go.dev/dl/?mode=json): HTTPS plus published SHA-256, no GPG — the same trust posture as the musl Node path. The pull preserves (or sets) go.goVersion in the runtime entry.
Generating a Docker image: devtools dockerfile
If you publish a container image of your wrapper with every tool pre-installed, devtools dockerfile generates an optimized multi-stage Dockerfile from your config's tool list — so you never hand-write or hand-tune it.
datamitsu devtools dockerfile -o docker/Dockerfile
What it generates. A config-free shared base, then a config-split stage, then one build stage per binary app, per managed runtime, and per runtime-managed app (each inheriting its runtime stage). The final stage assembles the populated datamitsu store with COPY --link, one layer per app, and carries the full config for the entrypoint.
Two layers of cache isolation. Each app is its own COPY --link layer, so bumping a single app re-pulls only that layer instead of the whole image (pull-time, for your users). The generator also isolates the build: the base never carries the config, and the config-split stage slices the config into one minimal per-stage file (via devtools split-config) that each stage loads on its own. So editing one app — or regenerating/reformatting the whole config — re-runs only the cheap split plus the stages whose slice actually changed, instead of reinstalling every tool from scratch. Changing a runtime rebuilds that runtime and every app under it; changing the base datamitsu image rebuilds everything.
Each app slice defines its entire dependency closure and every referenced
runtime, including pnpm. The stage's normal install <app> installs that
closure, so verification can execute the app safely. Each app still has its own
stage, including conventionally private names, and the final image copies each
app's own store subtree with COPY --link. A dependency edit rebuilds dependent
slices as well as the dependency's stage. Build-only Go and pnpm runtimes remain
out of the final image; run-time interpreters and UV's Python tree remain in it.
If Linux/libc filtering would remove a dependency of an included app, generation
fails with the root and missing dependency named. Unsupported architectures
fail the stage's install with the binary name and target platform, including
with --no-verify; no successfully built image can omit that dependency.
Base image and digest pinning. The base image is ghcr.io/datamitsu/datamitsu at the version of the datamitsu binary you run the command with — not your package.json. That tag is resolved to a SHA-256 digest and pinned as FROM …@sha256:… so builds are reproducible.
Pinning is best-effort and never fails the command. If the registry is unreachable, you pass --offline, or you build against a non-release (dev/unstable) datamitsu, the FROM line is left unpinned and a warning is written both into the generated file and to stderr:
datamitsu devtools dockerfile -o docker/Dockerfile --offline
The generated file is fully overwritten on every run — there are no managed regions. It is a generated artifact you own: hand-edit it freely, but re-running the command discards those edits. Commit the generated file and protect it with the drift check below.
Build-time verification (on by default). Each app stage runs the app's version check right after installing it, so an app that doesn't actually run fails the Docker build instead of shipping a broken image. This is the safe default; pass --no-verify to turn it off (for example, to speed up emulated cross-arch builds):
datamitsu devtools dockerfile -o docker/Dockerfile --no-verify
Alpine variant. Pass --alpine to target the musl base image (…:<version>-alpine):
datamitsu devtools dockerfile -o docker/Dockerfile.alpine --alpine
OCI labels. Supply your image's labels with repeatable --label flags. Keep them in the generate invocation (e.g. a Taskfile task) so they survive regeneration:
datamitsu devtools dockerfile -o docker/Dockerfile \
--label org.opencontainers.image.title=my-config \
--label org.opencontainers.image.source=https://github.com/me/my-config
Build args and environment variables. Declare build-time ARGs with repeatable --arg flags and runtime ENV vars with --env. In the final stage all ARGs are emitted before all ENVs, so an ENV value can reference an ARG. An --arg may be a bare NAME (its default is supplied by docker build --build-arg NAME=…) or NAME=default; an --env value may contain = (only the first separates key from value). Like labels, keep them in the generate invocation so they survive regeneration:
datamitsu devtools dockerfile -o docker/Dockerfile \
--arg BUILD_DATE \
--arg TZ=UTC \
--env LANG=C.UTF-8
This emits, in the final stage:
ARG BUILD_DATE
ARG TZ="UTC"
ENV LANG="C.UTF-8"
Build-time variables (--build-arg). --arg/--env only affect the final image — they are runtime knobs and do nothing for the build itself. To influence the install stages (for example raise the install timeout for heavy tools), use --build-arg. Each one is declared as an ARG (overridable via docker build --build-arg) and promoted to an ENV in a dedicated dm-build stage that every install stage derives from, so datamitsu install sees it. The final image is built FROM dm-base (not dm-build), so build args never leak into the shipped image:
datamitsu devtools dockerfile -o docker/Dockerfile \
--build-arg DATAMITSU_INSTALL_TIMEOUT=1200 \
--build-arg HTTP_PROXY
emits:
FROM dm-base AS dm-build
ARG DATAMITSU_INSTALL_TIMEOUT="1200"
ENV DATAMITSU_INSTALL_TIMEOUT=$DATAMITSU_INSTALL_TIMEOUT
ARG HTTP_PROXY
ENV HTTP_PROXY=$HTTP_PROXY
FROM dm-build AS rt-go # install inherits the ENV
RUN datamitsu install --runtime go
A bare --build-arg NAME (no default) takes its value from docker build --build-arg NAME=…. With no --build-arg flags the dm-build stage is omitted and install stages derive straight from dm-base.
:::warning Secrets
Do not pass tokens or other secrets via --build-arg: an ARG value lingers in the intermediate build layers (visible in the build cache). For credentials use RUN --mount=type=secret instead.
:::
libc filtering and --force-include. The image targets one libc — musl when you pass --alpine, glibc otherwise. Binary apps are filtered to those that ship a binary for that libc on every architecture they declare; a glibc-only binary cannot execute on a musl image, so such apps are dropped from the generated Dockerfile rather than failing the build. Each generation prints the dropped apps:
Warning: excluded 47 app(s) with no musl binary (add via --force-include if universal): actionlint, age, … swag, …
Runtime-managed apps (bun/node/uv/jvm/go) are never filtered — their runtime provides the libc.
Many dropped tools are actually universal: a statically-linked Go binary recorded as glibc (or a static-musl Rust binary recorded as musl) runs fine on the other libc, but the registry under-declares it. Triage the warning list and add the genuinely-universal ones back with --force-include (keep it in the generate invocation so it survives regeneration):
datamitsu devtools dockerfile -o docker/Dockerfile.alpine --alpine \
--force-include jq,shfmt,golangci-lint,hadolint
A truly libc-specific tool (e.g. a dynamically-linked glibc binary like swag) should stay excluded — install it another way on that variant, or fix its registry entry if a musl build does exist.
Pulling from a mirror. To resolve the base-image digest from a registry other than ghcr.io (for example a pull-through cache), set DATAMITSU_OCI_REGISTRY:
DATAMITSU_OCI_REGISTRY=mirror.internal datamitsu devtools dockerfile -o docker/Dockerfile
Keeping the generated Dockerfile fresh in CI
Because the output is fully generated, add a drift check that fails when the committed Dockerfile is stale. Run the generator with --offline so a re-pushed upstream tag (a moving digest) cannot cause false failures — the check then compares structure and version, not the live digest:
- name: Check Dockerfiles are up to date
run: |
datamitsu devtools dockerfile -o docker/Dockerfile --offline
datamitsu devtools dockerfile -o docker/Dockerfile.alpine --alpine --offline
git diff --exit-code -- docker/Dockerfile docker/Dockerfile.alpine \
|| { echo "Dockerfiles are stale; run the generator and commit."; exit 1; }
In your publish workflow, generate with pinning enabled (drop --offline) so the pushed image's FROM is digest-pinned.
Testing After Updates
Verify cross-platform integrity
After updating any tool or runtime versions, run verify-all to check integrity across all platforms:
datamitsu devtools verify-all
This command:
- Downloads and hash-verifies all binary apps for every configured platform
- Downloads and hash-verifies all managed runtime binaries
- Installs runtime-managed apps (Bun, Node, UV, JVM, Go) on the current platform
- Runs version checks to confirm tools execute correctly
Useful flags:
# Skip version checks (faster, hash-only verification)
datamitsu devtools verify-all --no-version-check
# Increase download concurrency
datamitsu devtools verify-all --concurrency 8
# Skip entries that passed on the last run with unchanged config
datamitsu devtools verify-all --skip-passed
# Machine-readable output for CI pipelines
datamitsu devtools verify-all --json
# Skip remote config resolution
datamitsu devtools verify-all --no-remote
Results are cached incrementally in a state file. Using --skip-passed skips entries whose configuration fingerprint hasn't changed since the last successful verification, which speeds up repeated runs during development.
Local smoke test
Always test locally before publishing:
# Re-download everything
datamitsu init
# Run the full check pipeline
datamitsu check
# Verify specific tools
datamitsu exec prettier -- --version
datamitsu exec eslint -- --version
Bumping a tool whose output is parsed
A cached lint pass means the tool's
outputParser read its output
and found nothing. A release that changes the output format can turn every finding into
"nothing" for a parser that no longer recognizes it, and the run then looks clean and is
cached as clean. Each parser the reference configuration wires therefore has a clean and
a finding-bearing recording of its real tool in the datamitsu repository
(parsers/datamitsu-parsers/fixtures/<parser>/), asserted by the parser's tests.
When a bump changes the version of such a tool, record both runs again with the new version and run the parser tests before the bump ships:
# In the datamitsu repository: rerun the tool over a clean input and over one with
# findings, write stdout, stderr and the exit code into the fixture files, then run
# the tests from parsers/, where rustup picks up the pinned Rust release
(cd parsers && cargo test)
parsers/datamitsu-parsers/fixtures/README.md lists the arguments each recording uses.
A findings recording that parses to nothing fails the test: the parser needs a fix, in
a module release, before the configuration can rely on the new version.
Automation
GitHub Actions: periodic version checks
Set up automated version checking with a scheduled GitHub Actions workflow:
name: Check for tool updates
on:
schedule:
- cron: "0 9 * * 1" # Every Monday at 9:00 UTC
workflow_dispatch: # Allow manual triggers
jobs:
check-updates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup datamitsu
run: |
# Install datamitsu (adjust for your setup)
go build -o datamitsu .
# Note: pull-releases does not support --dry-run.
# Use pull-releases --update in the automated update PR workflow instead.
- name: Check node app updates
run: ./datamitsu devtools pull-node apps/nodeApps.json --update --dry-run
- name: Check UV app updates
run: ./datamitsu devtools pull-uv apps/uvApps.json --update --dry-run
- name: Check runtime updates
run: ./datamitsu devtools pull-runtimes runtimes/runtimes.json --update --dry-run
GitHub Actions: automated update PR
For a more hands-off workflow, create a workflow that applies updates and opens a pull request:
name: Update tool versions
on:
schedule:
- cron: "0 9 1 * *" # First day of each month
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup datamitsu
run: go build -o datamitsu .
- name: Update binary apps
run: ./datamitsu devtools pull-releases apps/binaryApps.json --update
- name: Update node apps
run: ./datamitsu devtools pull-node apps/nodeApps.json --update
- name: Update UV apps
run: ./datamitsu devtools pull-uv apps/uvApps.json --update
- name: Update runtimes
run: ./datamitsu devtools pull-runtimes runtimes/runtimes.json --update
- name: Verify all platforms
run: ./datamitsu devtools verify-all
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
title: "chore: update tool versions"
body: "Automated tool version update. Review changes and verify locally before merging."
branch: chore/update-tool-versions
commit-message: "chore: update tool versions"
Best Practices
Semantic versioning
Follow semantic versioning for your wrapper package releases:
- Patch (1.0.x): tool version bumps with no configuration changes
- Minor (1.x.0): new tools added, new features in configuration
- Major (x.0.0): tools removed, breaking configuration changes, minimum version bumps
Changelog
Keep a changelog documenting what changed in each release:
- Which tools were updated and to what version
- Any new tools added or removed
- Configuration changes that users need to be aware of
- Minimum datamitsu version changes (
getMinVersion())
Update workflow
A typical update cycle looks like this:
- Run
devtools pull-*commands to detect and apply updates - Regenerate lock files for any updated Bun, Node, UV, or Go apps
- Run
devtools verify-allto check cross-platform integrity - Run
datamitsu init && datamitsu checklocally - Commit, push, and create a release
Migration guides
When making breaking changes (removing tools, changing configuration structure), provide a migration guide in your release notes explaining:
- What changed and why
- Step-by-step instructions to update
- The new minimum datamitsu version if changed
Configuring pnpm Settings for Node Apps
Node apps run pnpm install in an isolated directory managed by datamitsu. datamitsu always writes a pnpm-workspace.yaml containing secure supply chain defaults before pnpm install runs. You can override or extend those defaults by supplying your own pnpm-workspace.yaml via App.files — datamitsu shallow-merges your keys on top of the baseline.
See the Supply Chain Security guide for the full list of baseline settings and what each one does.
:::caution Files are written only on initial install
App.files (including pnpm-workspace.yaml) are read on every install, but the install directory is only rebuilt when the app's config hash changes. If you change files after an app is already cached, run datamitsu store clear (or delete the app's subdirectory under .apps/node/) to force a reinstall.
:::
Apps without build scripts (zero config)
No App.files["pnpm-workspace.yaml"] is required. datamitsu writes the secure defaults automatically:
const eslint = {
node: {
packageName: "eslint",
binPath: "node_modules/.bin/eslint",
version: "9.17.0",
lockFile: "br:...",
},
};
Apps that need build scripts (puppeteer, sharp, esbuild, etc.)
pnpm (11 and later) blocks lifecycle scripts by default. If install fails with ERR_PNPM_IGNORED_BUILDS, allowlist the package via allowBuilds:
const mmdc = {
files: {
"pnpm-workspace.yaml": YAML.stringify({
allowBuilds: { puppeteer: true },
}),
},
node: {
packageName: "@mermaid-js/mermaid-cli",
binPath: "node_modules/.bin/mmdc",
version: "11.15.0",
lockFile: "br:...",
},
};
The merged result written to the app environment keeps all secure defaults (strictDepBuilds: true, minimumReleaseAge: 10080, etc.) plus the user's allowBuilds entry. Only the keys you explicitly set are overridden.
After adding allowBuilds, regenerate the lock file so pnpm can record the approved builds:
pnpm exec datamitsu config lockfile mmdc
Overriding a security key
User intent wins on every key. Setting strictDepBuilds: false disables strict build approval — use only when you understand the risk:
files: {
"pnpm-workspace.yaml": YAML.stringify({
strictDepBuilds: false,
}),
}
:::note Bun, Node, and UV apps
For Bun and Node apps, any package.json included in App.files will be overwritten by the datamitsu core when it writes the managed package.json. Both app kinds can use pnpm-workspace.yaml for customization — the core merges that file with secure defaults rather than overwriting it.
For UV apps, project-level settings are configured via pyproject.toml. However, any pyproject.toml included in App.files will be overwritten by the datamitsu core when it writes the managed pyproject.toml. This customization path is not available for UV apps.
:::