The same five chores are copied into every repository's Makefile or justfile with small differences: fetch development secrets, log in to the container registry, create a new service from the template, check toolchain versions, open the service's dashboards. Some copies were fixed when the registry moved and some were not, so make registry-login fails with Error response from daemon: Get "https://old-registry.acme.dev/v2/": dial tcp: lookup old-registry.acme.dev: no such host in half the repositories. An internal developer CLI puts those shared operations in one versioned tool, while each repository keeps its own tasks. This page builds one, as part of task runners and an internal developer CLI.

The goal is a small tool — often a few hundred lines — not a platform. It should do a handful of things well and delegate everything repository-specific.

Diagnostic

Find the operations duplicated across repositories and how far the copies have diverged:

#!/usr/bin/env bash
set -euo pipefail
root="${1:-$HOME/src}"
for f in "$root"/*/{Makefile,justfile,Taskfile.yml}; do
  [ -f "$f" ] || continue
  grep -hoE '^(registry-login|secrets-pull|new-service|doctor|dashboards)[: ]' "$f" 2>/dev/null | tr -d ': ' | sed "s|^|$(basename "$(dirname "$f")") |"
done | sort -k2 | awk '{c[$2]++} END {for (t in c) print c[t], t}' | sort -rn
grep -lr 'old-registry.acme.dev' "$root"/*/{Makefile,justfile,Taskfile.yml} 2>/dev/null | wc -l | xargs echo "files still using old registry:"

Expected bad output across 23 repositories:

21 doctor
19 registry-login
14 secrets-pull
6 new-service
4 dashboards
files still using old registry: 11

The same task exists in up to 21 places, and eleven copies still point at a registry that no longer exists.

Copies of Each Shared Task Across Repos Bar chart showing how many repositories contain their own copy of each shared task. Copies of Each Shared Task Across Repos doctor 21 repos registry-login 19 repos secrets-pull 14 repos new-service 6 repos dashboards 4 repos
Every copy is a place a fix has to be repeated; eleven registry copies were already stale.

Root cause

Task runners are scoped to one repository by design, which is right for repository-specific work and wrong for organisation-wide operations. Without a place to put shared operations, each team copies the nearest working example and adapts it, and from that moment the copies evolve independently. Infrastructure changes — a new registry, a new secret manager path, a renamed SSO profile — then require a coordinated change across every repository, which never quite completes. The fix is structural: move shared operations into one tool with one owner and one release process, and have repositories call it rather than reimplement it.

The opposite failure is also common: an internal CLI that tries to replace repository task runners entirely, forcing every project's build and test logic into a central codebase. That makes the platform team a bottleneck for every repository change. Keeping the CLI to shared operations plus delegation avoids it.

Resolution

  1. Start with a small, typed codebase. Python with Typer, Go with Cobra, or Node with Commander all work; pick the language the platform team maintains best. A Python skeleton:
import os
import shutil
import subprocess
import typer

app = typer.Typer(help="acme: shared developer workflows")
REGISTRY = os.environ.get("ACME_REGISTRY", "registry.acme.dev")

def run(*cmd: str) -> None:
    subprocess.run(cmd, check=True)

@app.command()
def registry_login() -> None:
    """Log in to the container registry with your SSO session."""
    token = subprocess.run(["acme-sso", "token", "--audience", REGISTRY], check=True, capture_output=True, text=True).stdout.strip()
    subprocess.run(["docker", "login", REGISTRY, "-u", "oauth2", "--password-stdin"], input=token, text=True, check=True)

@app.command()
def doctor() -> None:
    """Check shared tools, then run the repository's own doctor task."""
    for tool in ("docker", "git", "just"):
        typer.echo(f"{tool:8} {'ok' if shutil.which(tool) else 'MISSING'}")
    for runner in (("just", "doctor"), ("task", "doctor"), ("make", "doctor")):
        if shutil.which(runner[0]) and any(os.path.exists(f) for f in ("justfile", "Taskfile.yml", "Makefile")):
            run(*runner)
            break

if __name__ == "__main__":
    app()
  1. Delegate repository work instead of reimplementing it. acme doctor runs shared checks and then the repository's own doctor task, so each repository extends the check rather than copying it.

  2. Distribute it like any tool, with a pinned version. Publish to an internal package index or a Homebrew tap, and pin the version in each repository's toolchain file so upgrades are deliberate:

#!/usr/bin/env bash
set -euo pipefail
pipx install --index-url https://pypi.acme.dev/simple "acme-cli==1.8.2"
acme --help
mise use "pipx:[email protected]"
  1. Replace the copies. In each repository, change the duplicated tasks to one-line calls, then delete them once documentation points at acme directly:
# Log in to the container registry (shared implementation)
registry-login:
    acme registry-login
  1. Measure what fails. Record anonymous command names, durations and exit codes to an internal endpoint (with an opt-out), so the platform team sees which commands fail most and where onboarding breaks:
import json
import time
import urllib.request

def report(command: str, started: float, code: int) -> None:
    if os.environ.get("ACME_TELEMETRY") == "off":
        return
    body = json.dumps({"cmd": command, "ms": int((time.time() - started) * 1000), "exit": code}).encode()
    req = urllib.request.Request("https://telemetry.acme.dev/cli", data=body, headers={"content-type": "application/json"})
    try:
        urllib.request.urlopen(req, timeout=2)
    except OSError:
        pass
Shared Operations in One Tool Flow from duplicated repository tasks to a versioned CLI that repositories call and that reports failures. Shared Operations in One Tool repo copies 19 variants acme CLI one implementation pinned version per repository telemetry failures by cmd
One implementation, pinned per repository, with telemetry showing where it breaks.

Expected output

$ acme --help
Usage: acme [OPTIONS] COMMAND [ARGS]...
  acme: shared developer workflows
Commands:
  doctor          Check shared tools, then run the repository's own doctor task.
  registry-login  Log in to the container registry with your SSO session.
  secrets-pull    Fetch development secrets for this repository.
  new-service     Create a new service from the golden template.
$ acme registry-login
Login Succeeded

The registry login works identically in every repository, and fixing it again in future means one release rather than twenty-three pull requests.

Telemetry gives the platform team a feedback loop that task files never had. A dashboard of the last week's commands, sorted by failure rate, usually reveals one or two operations that fail for a specific group — a region with a different SSO endpoint, a laptop model missing a dependency — long before anyone files a ticket. Treat that list as the CLI's backlog.

Prevention

  1. Give the CLI an owner and a changelog. Shared tools without an owner become the next set of stale copies.

  2. Test commands in CI against a clean container, including acme doctor and acme new-service end to end, on every release.

  3. Warn on old versions. At startup, compare the running version with the version pinned in the current repository and print the exact upgrade command when they differ.

Rolling Out the CLI Across Repositories Ordered rollout steps from inventory to deleting the last duplicated task. Rolling Out the CLI Across Repositories 1 — inventory duplicated tasks 2 — build CLI for shared operations 3 — publish and pin a version 4 — replace copies with one-line calls 5 — delete forwarding tasks after docs change
Repositories keep working at every step because tasks forward to the CLI before they are removed.

Platform caveats

Windows (native): pipx and Python work natively, but commands that shell out to Bash do not. Keep the CLI's own logic in Python or Go rather than embedded shell, and test on a Windows runner.

macOS: Homebrew taps are the most familiar distribution for macOS developers; brew install acme/tools/acme plus a pinned formula version works well alongside mise or Devbox.

Apple Silicon (ARM64): Go and Rust CLIs need separate darwin/arm64 builds; Python CLIs are architecture-independent unless they bundle native extensions.

Rollback

Repositories that still have their forwarding tasks can revert them to the previous inline implementation from git history; the CLI itself can be uninstalled without affecting anything else:

#!/usr/bin/env bash
set -euo pipefail
git revert --no-edit "$(git log -1 --format=%H -S 'acme registry-login' -- justfile)"
pipx uninstall acme-cli

Frequently Asked Questions

Should the internal CLI replace each repository's task runner?

No. Keep repository-specific build, test and run tasks in each repository. The CLI should own only operations that are the same everywhere and delegate the rest, so teams are not blocked on the platform team for their own tasks.

Which language should we build it in?

The one the owning team maintains best. Python with Typer is quick to write and easy to distribute with pipx; Go produces single static binaries that suit Windows and minimal environments.

Is telemetry acceptable for an internal tool?

Usually, if it is limited to command names, durations and exit codes, documented, and has an opt-out. Never record arguments, paths or environment variables, which may contain secrets.

How do we keep developers on current versions?

Pin the version in each repository's toolchain file and bump it deliberately. Add a startup check that prints the upgrade command when the installed version differs from the pinned one.