Mise + GitHub CI Standards
This guide is the source of truth for local tool setup, Bun shell scripts, and
GitHub Actions. If an agent edits mise.toml, .pre-commit-config.yaml,
scripts under scripts/, or .github/workflows/*.yml, it MUST read this file
first.
Goals
- A new contributor installs only
mise;mise installinstalls project tools, andmise run doctorproves the workstation is usable. - Local checks and CI execute the same named
misetasks. CI is not a second copy of cargo / formatter / linter commands. - Rust stays under
rustupandrust-toolchain.toml; mise does not manage Rust for this repo. - Local mise tasks put Rust artifacts in the user’s XDG cache under
lake/target, rather than in each jj workspace. Cargo fingerprints select compatible artifacts, and Cargo’s target-directory lock serializes concurrent writers, so isolated Jujutsu workspaces reuse dependencies instead of each retaining a complete build tree. The cache is untracked, contains no credentials or source data, and may be deleted to force a cold rebuild. CI remains ephemeral and sets its own incremental policy. Tests that inspect repository artifacts must also resolve them from the invocation workspace, never compile-time source paths. - Local-first: the comprehensive gate runs LOCALLY (
mise run ship, which runsmise run ci— gate + dependency policy + doc + spec-selftest + LocalStack integration — then a conventional-commit check, then push through the jj-pre-push fmt/clippy gate). CI (ci.yml) triggers onpush: [main]+workflow_dispatchonly; it is a post-merge Linux backstop, NOT run on PRs and NOT the first place checks run. Local covers more than CI (Docker, no ephemeral limits). - TypeScript scripts use Bun Shell deliberately: safe interpolation, explicit error handling, and structured parsing instead of fragile text pipelines.
Ownership
mise.tomlowns tool versions, shared environment variables, and task names.rust-toolchain.tomlowns the stable Rust toolchain and stable components.scripts/*.tsowns any task logic longer than one shell command..pre-commit-config.yamlowns hook wiring, but hook commands should call the same underlying commands exposed bymise.toml..github/workflows/ci.ymlowns GitHub-specific orchestration only: checkout, Rust bootstrap, mise bootstrap, caching, permissions, concurrency, and calls tomise run ....
Tool Version Rules
- Pin CI-critical tools to concrete versions in
mise.toml. Do not uselatestforbun,uv,jj,gh,prek,agent-spec,cargo-deny,cargo-shear,cargo-nextest, orprotocunless the PR is explicitly a toolchain refresh and records the reason. - Top-level
[tools]is the base developer environment. Do not put deploy-only tools (cloud emulators, load-test tools) there; attach them to the deploy tasks that need them. - Tool bumps are their own chore unless a feature genuinely requires them.
- After changing
[tools], runmise install,mise ls --current, andmise run doctor. - Do not add Rust to
[tools]. Install stable / nightly withrustup; keep the stable channel inrust-toolchain.toml.
Task Rules
mise run doctoris the first command in a new session. It may warn about optional GitHub workflow gaps, but it must fail for missing required tools or a broken Rust build.mise run gateis the fast local push gate. It must include hooks, Rust tests, the e2e self-check, andsite-checkfor the Astro site and rendered documentation.mise run test-adbcserializes its test functions because each launches a bounded upstream-driver subprocess and Query fixture. This preserves the ADBC deadline whilegatecontinues running independent tasks in parallel.mise run test-integrationowns checkout-scoped LocalStack lifecycle;mise run test-integration-externalruns the identical ignored-only package suite against a caller-managed endpoint and is the GitHub CI entry point. Both exclude the Apache Iceberg REST fixture test; its environment is not a LocalStack fallback.mise run test-iceberg-integrationowns an Apache Iceberg REST Catalog plus MinIO lifecycle and runs the real ignored interoperability test. It belongs to the comprehensivecitask, not the fastgate; its external variant consumes caller-managed fixture endpoints.mise run ciis the full CI gate. It must includegate, both dependency policy tasks, Rustdoc warnings, and spec tooling self-tests.- If a CI check protects a repo invariant, expose it as a
misetask and run it fromci; include it ingateonly when it belongs in the fast local loop. - Lane-1 work also runs
mise run spec-lifecycle <spec>. spec-lifecyclediscovers changed paths throughjj diff, not Git’s worktree view, so its boundary check is scoped to the current colocated Jujutsu workspace.site-checkmust start frombun install --frozen-lockfile, then typecheck, lint, format-check, build, index, and smoke-test the static output. This keeps local and Pages builds on the same dependency graph.- Task names are part of the agent contract. Rename a task only with matching
updates to
AGENT.md,CLAUDE.md, workflow docs, hooks, and CI. - Parameterized tasks use mise’s
usagefield and${usage_name?}environment variables. Do not use deprecated{{arg(...)}},{{option(...)}}, or{{flag(...)}}templates. - Keep
mise.tomldeclarative. Move loops, parsing, and multi-step logic intoscripts/*.tsand invoke those scripts from tasks.
Bun Shell Rules
- Scripts start with
#!/usr/bin/env bunand importimport { $ } from "bun";only when they actually execute external commands. - Prefer “$`cmd ${arg}``` interpolation over string-built shell commands. Bun treats interpolated strings as single literal arguments, which prevents normal shell injection.
- Do not use
${{ raw: value }}unless the input is a compile-time constant in the script. Raw interpolation is an escape hatch, not a convenience. - Do not pass user or repo-derived strings through
bash -c,sh -c, or another shell interpreter. If that is unavoidable, validate every argument before it crosses into that shell. - Remember that escaping is not authorization. External programs can still
treat a safe literal string as a flag, e.g.
--upload-pack=...; validate values that become command arguments. - Use
.text(),.json(), or.lines()when consuming output. Avoid parsing human-oriented command output when a structured API or machine-readable flag exists. - Use
.quiet()for probes where output is not evidence. Keep noisy command output for failure evidence or user-facing reports. - Use
.nothrow()only when non-zero exit codes are part of the expected control flow, and checkexitCodeimmediately. - Use
Bun.spawn([...])for pure pass/fail probes that do not need shell features. Use Bun Shell for pipelines, redirection, env assignment, and concise command composition. - Prefer
.cwd(path)and.env({...process.env, KEY: value})over global mutation of process state.
GitHub Actions Rules
- Use
jdx/mise-actionto install mise-managed tools in CI. Do not hand-install Bun, uv, agent-spec, cargo-nextest, protoc, prek, jj, or gh in workflow YAML. - Install Rust toolchains before
Swatinem/rust-cache, because the cache key depends on the active Rust version. - CI steps should call
mise run ci,mise run check-commits, or another named task. Do not duplicate cargo command lines in YAML. - Use least-privilege permissions. Normal CI uses
contents: read; jobs that comment, label, publish, or upload need explicit extra permissions. - Use workflow-level
concurrencykeyed by workflow + PR number/ref, withcancel-in-progress: true, so force-pushes do not burn runner time. - Every hosted job declares an explicit
timeout-minutesbudget. The Apache Iceberg REST integration job uses the same 30-minute cold-run margin as the LocalStack integration job; do not leave a Docker fixture to the platform’s default six-hour timeout. - Do not make CI depend on local-only state such as installed hooks, local data directories, or untracked files.
Conventional Commits
- jj does not run git hooks, so commit messages are enforced twice:
local
commit-msghook for git users, and the PR-only CI commit job. - The CI commit job should call a mise task that wraps
scripts/check-conventional-commit.ts --range <base>..HEAD. - The accepted format remains documented in
docs/guides/commit-style.md.
Repository releases
Release Please maintains one repository release for the entire lake workspace.
It does not publish the internal crates, which remain publish = false.
.github/workflows/release-please.ymlruns after a push tomainand on an hourly, off-the-hour reconciliation schedule. The scheduled run reuses the same idempotent Release Please authority after a transient GitHub API or Actions failure.workflow_dispatchremains the immediate recovery path when GitHub delayed or missed a push event; dispatch it onmain, never by hand-editing release files or tags.release-please-config.jsonuses thesimplestrategy because upstream release-please cannot currently process Cargo members that inheritversion.workspace = truethrough itscargo-workspaceplugin (upstream issue #2111).version.txtand.release-please-manifest.jsontrack the repository release version. TOML extra-file updaters keepworkspace.package.versionand every lake package entry inCargo.locksynchronized.- When a new crate is added, add its exact
Cargo.lockextra-file JSONPath using.name.valuetorelease-please-config.json.
The workflow uses GitHub’s short-lived built-in GITHUB_TOKEN; no long-lived
release credential is stored. This matches the repository’s local-first model:
generated release PRs do not trigger a separate pull-request workflow.
Maintainers review the version/changelog diff and run mise run gate before
merge, while the existing main-only CI remains the post-merge backstop.
Release Please continuously updates one release PR from Conventional Commits.
Merging that PR updates the changelog and versions, creates vX.Y.Z without a
component prefix, and publishes the matching GitHub Release. When that root
release is created, the same short-lived token automatically dispatches the
existing image workflow with the exact release tag. The image workflow still
checks out and validates that published immutable tag before it publishes the
multi-platform GHCR image. Its 180-minute job budget leaves room for a cold
QEMU build while making a stalled release a finite, actionable failure instead
of relying on the GitHub Actions default.
The release event created by GITHUB_TOKEN intentionally does not trigger a
second workflow, so this workflow_dispatch handoff is required rather than
optional wiring. For a release made before the handoff existed, run the manual
image backfill with the published release tag:
gh workflow run release-image.yml --ref main -f tag=vX.Y.Z
The workflow checks out two immutable revisions. release-source is always
the published tag and is the sole Docker build context; it is validated against
the GitHub Release target SHA and remains
org.opencontainers.image.revision. build-recipe is the workflow revision:
on a normal release event GitHub supplies the tag revision, while the documented
manual command dispatches main and therefore supplies the current
Cargo-chef Dockerfile to rebuild an older immutable source. The distinct
io.rararulab.lake.build-recipe.revision OCI label records that recipe SHA; it
does not change the source identity or release tag.
Cargo-chef derives recipe.json from that release-source context. When the
workspace recipe names a local path dependency, the builder must copy that
exact dependency from the planner before cargo chef cook; copying only the
recipe makes Cargo resolve a path that is absent from the builder filesystem.
Keep this transfer narrower than the later application-source copy so normal
source changes do not invalidate the exportable cooked-dependency layer. A
historical backfill repair is not complete until a native --target builder
build against the immutable tag reaches the builder stage, rather than merely
proving that the planner can emit a recipe.
Wait for the Publish release image run to complete, then resolve the
manifest-list digest and verify both linux/amd64 and linux/arm64 before
updating a production deployment.
Review Checklist
Before approving a PR that changes the toolchain, scripts, or CI:
mise.tomlremains the single task registry.- CI does not duplicate commands already represented by
misetasks. - Tool versions are pinned or the PR explicitly explains why a moving version is acceptable.
- Bun Shell scripts use safe interpolation and explicit error handling.
mise run doctor,mise run gate, and the relevant slower task (mise run ci,mise run doc, ormise run spec-selftest) pass locally.mise tasksoutput is still understandable to an agent readingAGENT.md.