Skip to content

tsops dev: blazing-fast preview start for PRs and sandbox-friendly agent workflows #63

Description

@Pom4H

Summary

Add a long-running tsops dev <namespace> mode (Skaffold-style) and content-addressed image caching, so that:

  1. New PRs reach a usable preview environment in seconds, not minutes — even on cold CI.
  2. AI coding agents running in restricted sandboxes (Claude Code on the web, GitHub-hosted Codespaces, etc.) get a one-command preview-loop they can launch before they even open a PR.
  3. CI workflows like worken-pr-preview.yml collapse onto the same code path as local dev, instead of reimplementing artifact-passing, digest-reuse, and namespace lifecycle in shell scripts per project.

Motivation

tsops already has most of the building blocks for an incremental, watch-driven workflow:

  • up <namespace> --apps-from-changes --base-ref — diff-driven app selection.
  • build --filter <ref> — incremental build by git ref.
  • core/dependencies/graph — app dependency graph.
  • core/operations/planner — diff-aware create/update/unchanged plan.
  • Separated builder / deployer / cert-hook / db-hook operations.

What's missing for a fast PR-preview experience:

  1. No long-running loop. Every push triggers a full up from scratch (clean checkout, clean BuildKit, full image build per included service). Even if only one file changed, all services in --include are rebuilt.
  2. No content-addressed image reuse. If worken-front source didn't change between PR pushes, we still rebuild it; the registry tag is keyed by commit SHA, not by source-tree hash.
  3. No "attach" mode for CI. Workflows have to glue: build artifacts → upload → download → manually pass image digests via env. Each consumer reinvents this. (See e.g. WORKEN_PREVIEW_IMAGE_DIGESTS_JSON in our worken-pr-preview.yml.)
  4. No file-sync. A one-character change in a TSX file rebuilds and redeploys the whole image, whereas a kubectl cp + Next.js fast-refresh would take ~1s.

The agent-in-sandbox angle makes (1)–(3) acute: an agent's iteration loop currently is "edit → push → wait several minutes for CI workflow to build → fight basic-auth on the preview URL → discover the change". We'd like to compress that to "edit → preview reflects the change", with the bulk of the work happening before CI is even invoked.

Proposal

A. tsops dev <namespace> — long-running watch loop

tsops dev <namespace>
  --var pr=<n> | --pr auto       auto = resolve from `gh pr view` / current branch
  --include <apps>               initial filter; defaults to --apps-from-changes
  --strategy <auto|sync|rebuild> per-event override
  --debounce <ms>                default 500
  --port-forward                 auto kubectl port-forward per app
  --once                         CI mode: up + first sync pass + exit 0
  --attach                       skip up; only attach watcher to existing namespace
  --no-build                     consume pre-built digests
  --image-digests <json>         { "<app>": "<repo>@sha256:..." }
  -c, --config <path>
  --dry-run

Loop semantics:

  1. Initial up (or attach) using existing planner.
  2. Watcher (e.g. chokidar) per app.build.context.
  3. Classifier maps each batch of events to an action:
    • sync — file matches app.dev.sync and not rebuildOn → tar-pipe via kubectl exec into the running pod (no sidecar required if the image has tar).
    • rebuild — matches rebuildOn (Dockerfile, lockfile, deps) → rebuild that single app + rolling-restart deployment.
    • redeploytsops.config.ts or k8s manifest changed → re-run planner, apply diff.
    • db-migrate — matches db-hook.watch (new field) → re-run db-hook.

B. app.dev config

defineConfig({
  apps: {
    'worken-front': {
      build: { context: 'apps/worken-front', dockerfile: '...' },
      dev: {
        sync: [
          { from: 'apps/worken-front/src',    to: '/app/apps/worken-front/src' },
          { from: 'apps/worken-front/public', to: '/app/apps/worken-front/public' },
        ],
        rebuildOn: ['**/Dockerfile', '**/package.json', 'bun.lock'],
        ignore: ['**/*.test.ts', '**/.next/**'],
        command: ['bun', '--hot', 'run', 'dev'],
        readinessProbe: { httpGet: { path: '/_health', port: 3000 } },
      },
    },
  },
})

Pure addition — existing apps without dev keep current behavior.

C. Content-addressed image cache

In operations/builder:

  1. computeSourceTreeHash(app) = stable hash over files in build.context minus .dockerignore, plus the app's slice of tsops.config.ts, plus the tsops version.
  2. Image gets two tags on push: <sha> (current) and srctree-<hash> (new).
  3. Before building, docker buildx imagetools inspect <repo>:srctree-<hash> — hit → return digest, skip build entirely.
  4. New flag --no-cache to opt out.

This is the single biggest win for PR previews: a 6-service preview where only one service changed becomes 1 build + 5 cache-hits.

D. --no-build --image-digests for CI

Today downstream projects glue this in shell. Lifting it into the CLI gives:

- run: tsops dev preview --once --no-build --image-digests "$DIGESTS_JSON" --var pr="$PR"

…which replaces a multi-step build-artifact / verify-artifact / up dance.

Why "blazing fast" for new PRs

End-to-end timeline for a typical PR with this in place:

Step Today With tsops dev + content-cache
Agent edits one file in worken-front dev watcher syncs file (~1s)
Agent pushes first commit n/a n/a
CI up triggered on PR open full build × N services + deploy content-hash hits for unchanged services, rebuild only changed → digest already in registry → dev --once --no-build → ~30s deploy
Subsequent PR push full rebuild of --include set content-hash skips unchanged + sync for hot path

The combination — content-hash cache plus --once --no-build — means CI does almost no work on the happy path. The agent or developer has effectively pre-built everything before CI ran.

Sandbox-friendly story for Claude / coding agents

Two scenarios, both improved:

Scenario 1 — agent has cluster credentials (rare). Run tsops dev preview --pr=auto in the sandbox; loop runs for the duration of the session.

Scenario 2 — agent has no cluster access (common). A SessionStart hook in the project triggers the trusted CI workflow with gh workflow run … on the agent's first commit, before the PR is even opened. Because that workflow now runs tsops dev preview --once --no-build --image-digests … against a content-addressed cache, by the time the agent opens the PR, the preview namespace is already up. Agent's iteration is bounded by git push latency, not by image build time.

This makes tsops directly useful as the substrate for "open a PR, get a working preview before the first review comment" — the experience Vercel/Netlify offer for static sites, but for full Kubernetes apps.

Non-goals

  • Replacing Skaffold/Tilt for users who already have them.
  • Hot-reloading compiled languages without language-specific support; sync is opt-in per app.
  • Running the preview cluster itself; tsops still assumes a reachable cluster.

Trade-offs

  • Content hash correctness. Stable hashing must respect .dockerignore and pin the tsops version into the hash; otherwise a tsops upgrade silently invalidates caches. Acceptable: fail closed on hash mismatch, verbose --explain-cache.
  • Sync requires tar in the dev image. Documented. Distroless prod images aren't affected — sync is dev-mode-only.
  • dev.command override means the dev-mode container differs from prod. Same constraint as Skaffold; the alternative (separate dev Dockerfile) is also supported via build.dockerfile overrides.
  • Watcher resource use on large monorepos. Mitigation: ignore globs, default-derived from .gitignore + .dockerignore.

Suggested incremental PRs

  1. feat(builder): content-addressed image cache via source-tree hash — standalone, immediate value, no API surface change beyond --no-cache.
  2. feat(cli): tsops up --no-build --image-digests — unify what downstream projects already do in shell.
  3. feat: tsops dev <namespace> — config schema, watcher, classifier, syncer, dev-loop. Largest PR; ships behind the new command, no behavior change for up/build/deploy.
  4. feat(cli): tsops dev --once — final piece that lets CI consume the same code path.

Alignment with current best practices for agentic dev speed

A short cross-check against published guidance from Anthropic, Skaffold/Tilt, and BuildKit, with concrete refinements to the design above.

1. Fast feedback is the dominant lever

Anthropic's 2026 Agentic Coding Trends Report and CodeScene both single out the speed of build/test/tool feedback as the highest-impact factor in agent productivity — agents converge on working solutions only as fast as they can verify them. The Claude Code best-practices doc calls verification ("tests, screenshots, expected outputs the agent can check itself") the highest-leverage practice.

Refinement: in addition to the human-facing watch loop, emit a stable, machine-parseable status file ({ phase, ready, urls, lastSyncMs, lastError }) that an agent can poll without scraping logs. Pairs naturally with --once.

2. Sync-vs-rebuild classification matches Skaffold/Tilt consensus

The community has converged on the exact rule encoded in section B:

  • Sync for interpreted code and static assets — pipe files into the running container, skip build/push/deploy.
  • Rebuild only on dependency-manifest changes (Dockerfile, lockfile, package.json).
  • For Node/Bun: run nodemon / bun --hot inside the container; never npm install on every change — gate it on package.json.

Refinement: default dev.ignore to .gitignore ∪ .dockerignore (Skaffold does this) so users don't have to re-list common ignores.

3. Content-addressed cache should sit on top of BuildKit, not replace it

The pattern in section C is well-established (the docker-image-context-hash GitHub Action does exactly this), and BuildKit's external cache backends (registry, gha, s3) are considered essential in CI because runners have no persistence between runs.

Refinements:

  • Use BuildKit's --cache-to type=registry,mode=max as the substrate for per-layer reuse, and treat the srctree-<hash> tag as a fast-path index on top of that — not a replacement.
  • Pin both the tsops version and the BuildKit frontend version into the hash; otherwise a frontend bump silently invalidates caches.
  • Ship --explain-cache from day one so misses are debuggable.

4. --once / --no-build mirrors skaffold render + skaffold deploy --images

Splitting CI build from deploy with pre-built digests is a documented Skaffold pattern. Lifting it into tsops is consistent with industry practice and removes per-project shell glue.

5. Sandbox-agent guidance from Anthropic

The Claude Code best-practices and sandbox guides recommend:

  • Run unsupervised agent loops inside Docker/sandbox isolation.
  • Pre-warm preview environments via a SessionStart hook so the preview is ready before the first review comment — exactly Scenario 2.
  • Cap iterations (maxTurns) and prefer preview-branch systems for review ergonomics.

Refinement: ship a documented SessionStart hook recipe in the tsops repo that runs gh workflow run … to kick off the preview before the agent opens the PR.

6. Spec-first / plan-mode caveat

Independent of tsops, the 2026 best-practice consensus is "spec-first, plan-mode, review every diff, ≥60% test coverage." A delivery-loop change like this lands its agent value only when those upstream practices are in place — worth a one-line note in the README so users don't expect speed to substitute for verification.

Net adjustments to fold into the PRs above

  • PR 1 (content cache): layer on BuildKit registry/GHA cache; include BuildKit frontend version in the hash; add --explain-cache.
  • PR 3 (tsops dev): default ignore from .gitignore ∪ .dockerignore; emit a structured status file the agent can poll.
  • Docs: add a SessionStart hook recipe for sandboxed agents; one-line note about spec-first being prerequisite.

References


Happy to prototype any of the four PRs against a fork if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions