@jetci/sdk-local
v0.1.4-alpha.3
Published
Readme
@jetci/sdk-local
@jetci/sdk-local contains all tools required to operate the CI locally on your dev machine.
The recommended user-facing setup is:
pnpm dlx @jetci/cliFor direct or unattended local-only setup, use:
pnpm dlx @jetci/sdk-local installThis installs the local SDK under .jetci, configures Git hooks, and adds @jetci/sdk-local to
.jetci/package.json under jetci.providers. The older install-hooks command remains as an alias.
Secrets
Local hook registrations can declare secrets:
onPreCommit({}, {secrets: ["NPM_TOKEN"]}, async ctx => {
console.log(ctx.secrets?.NPM_TOKEN)
})@jetci/sdk-local resolves declared secrets from environment variables with the same names and injects them into the event context. Jobs do not have a separate secrets API.
Jobs
@jetci/sdk-local jobs run through a host-controlled executor. By default, job commands run in Docker using node:22-bookworm-slim with network disabled. The image is configurable per job, so projects can use toolchain images such as .NET, Java, Rust, or Go:
const dotnetTest = defineJob(
"dotnet-test",
{image: "mcr.microsoft.com/dotnet/sdk:9.0"},
async ctx => {
await ctx.run("dotnet test")
},
)You can opt into local process execution for trusted workflows:
const test = defineJob("test", {environment: "local"}, async ctx => {
await ctx.run("pnpm test")
})Docker jobs are booted by the local host. The orchestrator and job job function stay in the host runtime, while ctx.run() executes inside the selected container with the repository mounted at /workspace. The host communicates with the container through Docker (docker run / docker exec), so custom images do not need Node or JetCI installed. Secrets are not exposed to jobs unless an orchestrator explicitly passes values in job params.
This mirrors the provider model expected for cloud runners: @jetci/sdk defines the job registration and executor hook, while each host decides how to satisfy that execution request. @jetci/sdk-local uses Docker; a future GitHub runner could map the same job metadata to GitHub-hosted jobs, self-hosted runners, container jobs, or another isolated compute backend.
Checkout
ctx.checkout(repo) creates an isolated source snapshot for the job and makes later ctx.run() calls default to that workspace. It does not run git checkout in the user's current working tree. For onPreCommit, the local SDK checks out the staged Git index so jobs see the content that is about to be committed, including staged-but-not-committed changes.
The local host resolves the requested RepoState to a commit with git rev-parse --verify, exports it with git archive, extracts it under .jetci/.internal/tmp/workspaces/<id>, and removes that workspace when the job finishes. Docker jobs execute commands from the matching /workspace/.jetci/.tmp/workspaces/<id> path.
RepoState.commit is the preferred checkout target. sha remains for compatibility, and ref, branch, or tag names are fallback metadata.
Package Managers
Job commands run as separate process invocations, so command-specific environment variables must be passed to each ctx.run() that needs them. In Node images, use Corepack before later pnpm commands:
await ctx.run("corepack enable pnpm")
await ctx.run("pnpm install --frozen-lockfile")
await ctx.run("pnpm -r build")If you install pnpm into PNPM_HOME, pass that env value to each command that uses pnpm. Docker shell commands automatically add PNPM_HOME to PATH when PATH is not explicitly provided. If you do provide PATH, do not use shell syntax such as $PATH inside the env value because Docker does not expand it before starting the command:
const pnpmEnv = {PNPM_HOME: "/pnpm"}
await ctx.run("corepack prepare pnpm@latest --activate", {env: pnpmEnv})
await ctx.run("pnpm config set store-dir .pnpm-store", {env: pnpmEnv})corepack use pnpm@latest can run an install through Corepack, but it does not reliably leave a pnpm executable available for subsequent ctx.run() calls in Docker images.
Artifacts
ctx.artifactUpload() accepts Node readable streams, Uint8Array, ArrayBuffer, files, and directories. Paths are relative to the current job workspace. Set zip: true on a path source to store it as a ZIP archive:
const artifact = await ctx.artifactUpload(
{path: "dist", zip: true},
{name: "dist.zip", persist: false},
)
await ctx.artifactDownload({
src: artifact,
path: "restored-dist",
unzip: true,
})Temporary artifacts are available to jobs in the same workflow run and removed when the listener finishes. Artifacts with persist: true remain under .jetci/.artifacts/persistent for later local runs. Both local-process and Docker jobs use the same artifact store.
Cache
Manual runs reuse durable memo and job-result cache entries by default. Pass --clear-cache to ignore existing entries and replace them with newly computed results. Pass --no-cache to ignore existing entries without saving new results:
pnpm dlx @jetci/cli run <event-key> --clear-cache
pnpm dlx @jetci/cli run <event-key> --no-cacheIDE and other structured clients can request the multiplexed event stream:
pnpm dlx @jetci/cli run <event-key> --event-stream=json-linesThis keeps one provider stdout while identifying orchestrator and worker lifecycle/output records separately. SDK
Local automatically attributes console.log, console.error, direct stdout/stderr writes, and ctx.run() child
output to the active worker. Normal CLI runs remain human-readable and do not enable event framing.
Structured worker events also carry repository-relative source locations when Node can resolve the workflow source
map. On failure, Local reports the innermost failing JobContext operation (ctx.run, ctx.checkout, artifact
operations, or ctx.withCache). An outer context helper preserves a location already attached by an inner helper,
allowing IDE clients to navigate to the actual failed CI line without coupling themselves to SDK Local.
Each context call also has an operation-started/operation-finished lifecycle, allowing test-style clients to
show ctx.* calls beneath their worker and attach output and navigation before a failure occurs.
IDE integrations can discover the locally configured listeners and jobs through the versioned JSON protocol:
pnpm dlx @jetci/cli inspectThe response is emitted on a line prefixed with JETCI_INSPECTION_JSON= and contains protocolVersion,
configurations, and jobs. Manual-trigger configurations include runnable: true and their manualKey;
other event listeners remain visible but cannot be invoked manually. Consumers should ignore other output and
read the last prefixed line, allowing discovery diagnostics to remain human-readable and backwards compatible.
Enabled first- and third-party providers can be discovered without executing them:
pnpm dlx @jetci/cli providersThe last JETCI_PROVIDERS_JSON= line contains validated static provider manifests for the package specifiers
explicitly listed in .jetci/package.json. See the provider protocol for the
manifest, trust, inspection, and execution contracts.
ctx.withCache() restores paths from the newest immutable raw-directory generation before running the callback. New cache data is prepared in a staging directory and published with an atomic rename only when the configured save rule allows it.
await ctx.withCache({
key: "pnpm-node-modules",
paths: ["node_modules"],
restoreKeys: ["pnpm-"],
save: "on-success",
}, async () => {
await ctx.run("pnpm install --frozen-lockfile")
await ctx.run("pnpm -r build")
})Cache paths must be relative to the job workspace. restoreKeys are prefix fallbacks, with the newest matching generation restored first. Each cache key retains its two newest generations. Per-key filesystem locks protect concurrent restores, publication, and cleanup. A failed save: "on-success" callback never publishes its staging directory, so the previous good generation remains intact. save: "always" still publishes callback output after failure. Existing .tar.gz and legacy directory caches remain readable as compatibility fallbacks, but all new saves use raw generations under .jetci/.cache/<key>.cache/generations.
