# AGENTS.md
This file provides guidance for contributors and AI assistants working with this repository.
## Project Overview
Telepresence is a Kubernetes development tool that enables fast local development by connecting your local workstation to a Kubernetes cluster. It allows developers to run services locally while accessing cluster resources and intercepting traffic from the cluster to their local machine.
## Git Workflow
- Never commit directly to the `release/v2` branch. Always create a feature branch with a name following the pattern `username/topic` (e.g., `thallgren/fix-dns-resolution`).
- All commits must be signed and signed-off (`git commit -s -S`).
- Limit commit message subjects to 72 characters. Do not wrap subjects;
rewrite them shorter instead. Wrap commit message body lines at 72
characters by default unless preserving exact external text requires a
longer line.
- **Always run `make lint` and fix every reported issue before pushing.** This is non-negotiable β CI runs the same linters and a push with lint errors wastes a CI cycle. If `make lint` finds problems, fix them in the appropriate commit (use `git commit --fixup=<sha>` followed by `GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash --gpg-sign <base>` to fold them in) before pushing.
- Push the branch and create a pull request for review.
- Always merge PRs with a merge commit (never squash or rebase).
## Design Plans
Major work (multi-file changes, new features, refactors) starts with a written plan
under `docs/plans/<topic>/`, presented for review before implementation begins.
A plan is scaffolding for review, not a lasting artifact. It is removed in the last
commit on the PR that implements it. By then, everything in the plan must have been
implemented and documented, so the plan no longer has a purpose.
## Build Artifacts
The Open Source version of Telepresence consists of three artifacts:
**Client-side (runs on developer workstation):**
- **`telepresence` binary** - The same binary serves as CLI, user daemon, and root daemon.
- **`telepresence` Docker image** - Used as both user and root daemon when running `telepresence connect --docker`.
**Cluster-side (runs in Kubernetes):**
- **`tel2` Docker image** - Used by the traffic-manager deployment and injected as traffic-agent sidecars.
## Build Commands
```bash
# Set required environment variables
export TELEPRESENCE_VERSION=v2.x.x-alpha.0 # or use auto-generated version
export TELEPRESENCE_REGISTRY=local # 'local' for Docker Desktop, or 'ghcr.io/telepresenceio'
# Build the telepresence binary
make build
# Build Docker images (for local Kubernetes development)
make client-image # Client container image
make tel2-image # Traffic-manager/traffic-agent image
# Build everything for local development
make build client-image tel2-image
# Install to system
make install
# Clean build artifacts
make clean
make clobber # Also removes tools
```
Environment variables:
- `TELEPRESENCE_REGISTRY` (required) - Docker registry for images. Use `local` for docker-based Kubernetes, or `ghcr.io/telepresenceio` for the release registry.
- `TELEPRESENCE_VERSION` (optional) - Version string to compile into binaries and images. If not set, auto-generated from CHANGELOG.yml and source hash.
Run `make help` for more information.
### Building on Windows
Windows builds use `build-aux\winmake.bat` instead of `make` directly. Pass the same parameters as you would to make. The script runs make inside a Docker container with appropriate parameters for Windows binaries.
## Testing
```bash
# Unit tests
make check-unit
# Regression tests (requires a Kubernetes cluster; see the guide below)
make check-regression
# One regression area / suite / test β plain go test selection:
go test ./regression_test -run 'TestIntercept/HeaderFilter/Test_PathPrefix'
# Chart-value combinations, clusterless:
go test ./regression_test/golden
```
The regression suite in `regression_test/` is the integration-test
package: declarative memoized fixtures, warm-cluster adoption for fast
scoped runs, coverage instrumentation, and a bidirectional
compatibility subset. **Read `regression_test/README.md` before writing or
debugging these tests** β it documents the fixture engine's rules (lazy
accessors, Mutate discipline, spec declarations), the RTEST_* environment,
the manager/workload catalogs, labels and platform constraints, coverage,
and compat runs.
## Linting
```bash
# Run all linters
make lint
# Run Go linter only
make lint-go
# Run protobuf linter only
make lint-rpc
# Run documentation linter only (link/nav consistency via tools/src/docslint,
# terminology and stale references via Vale in Docker; config in .vale.ini)
make lint-docs
# Auto-fix lint issues
make format
```
Linting uses golangci-lint v2 running in Docker. Configuration is in `.golangci.yml`.
## Code Comments
Comments must describe the code as it is. Never write comments that describe a
transition β why code was moved, what it replaced, or how it differs from an
earlier version. The reader sees only the current code, so such comments carry
no information for them. Keep comments short; avoid long explanations.
On internal (unexported) functions and methods, keep doc comments minimal: a
few lines stating only what the code cannot show, such as a locking-order or
publication-order invariant. With well-named code, the details live in the
code itself; a reader who wants them will read it. Multi-paragraph comments
that justify design decisions belong in review discussions, not in the
source.
## Code Generation
```bash
# Regenerate protobuf and license files
make generate
# Regenerate protobuf files only
make protoc
# Regenerate documentation files (after changing CHANGELOG.yml)
make docs-files
```
**Important:** After modifying `CHANGELOG.yml`, always run `make docs-files` to regenerate documentation files (`docs/release-notes.md`, `docs/release-notes.mdx`, `docs/variables.yml`).
**Important:** All files under `docs/reference/cli/` are generated from Go source code. Do not edit them directly; instead, modify the corresponding Go source and regenerate.
### Updating License Documentation
Run `make generate` and commit changes to `DEPENDENCY_LICENSES.md` and `DEPENDENCIES.md`.
## Documentation
The documentation under `docs/` aims to follow the
[DiΓ‘taxis](https://diataxis.fr/) framework. Its four quadrants map to the
layout like this:
| DiΓ‘taxis quadrant | Orientation | Location |
|-------------------|-------------|----------|
| Tutorials | learning | `docs/quick-start.md` |
| How-to guides | task | `docs/howtos/` |
| Reference | information | `docs/reference/` |
| Explanation | understanding | `docs/concepts/` |
When documenting a new feature, decide which quadrants it needs β typically a
how-to guide (how to enable/use it) plus a reference page (its complete
behavior, configuration, and limitations) β and keep the quadrants separate:
a how-to gets a task done and links to the reference for details; a reference
describes exhaustively and doesn't teach. Add new pages to the navigation in
`docs/doc-links.yml`, and run `make lint-docs` (link/nav consistency and
terminology) before pushing.
## Architecture
### Main Components
1. **CLI/Client** (`cmd/telepresence/`, `pkg/client/cli/`)
- Single binary serving as CLI, user daemon, and root daemon
- Commands are in `pkg/client/cli/cmd/`
2. **User Daemon (userd)** (`pkg/client/userd/`)
- Runs as the user, manages connection to traffic-manager
- Handles intercepts, port forwards, cluster communication
3. **Root Daemon (rootd)** (`pkg/client/rootd/`)
- Runs with elevated privileges
- Manages virtual network interface (VIF) and DNS
4. **Traffic Manager** (`cmd/traffic/cmd/manager/`)
- Runs in the Kubernetes cluster (ambassador namespace by default)
- Coordinates intercepts between clients and traffic-agents
5. **Traffic Agent** (`cmd/traffic/cmd/agent/`)
- Injected as sidecar into intercepted pods
- Routes traffic between the pod and the local machine
6. **Agent Init** (`cmd/traffic/cmd/agentinit/`)
- Init container for setting up iptables rules in pods
7. **Docker Network Driver** (`cmd/teleroute/`)
- Only used when connecting with `--docker` flag
- Provides the Docker network that enables communication between the Telepresence daemon container and other containers
### Key Packages
- `pkg/vif/` - Virtual network interface implementation
- `pkg/tunnel/` - gRPC-based tunneling for network traffic
- `pkg/dnsproxy/` - DNS resolution and proxying
- `pkg/agentconfig/` - Traffic-agent configuration
- `pkg/client/k8s/` - Kubernetes client interactions
- `pkg/routing/` - Network routing logic
- `pkg/client/cli/cmd/` - CLI commands. One per file.
### RPC Definitions
Protocol buffers are in `rpc/` with separate packages:
- `rpc/connector/` - Client-to-userd communication
- `rpc/daemon/` - Client-to-rootd communication
- `rpc/manager/` - Client/userd-to-traffic-manager communication
- `rpc/agent/` - Traffic-manager-to-traffic-agent communication
### Version Parity Between CLI and Daemons
The CLI never talks to a user or root daemon of a different version.
`pkg/client/cli/connect/version_check.go` enforces this on every command
that reaches a daemon: the host user daemon and root daemon must match the
client version exactly, and a containerized user daemon must match on
major.minor.patch. This means changes to `rpc/connector/` and `rpc/daemon/`
never need backward-compatibility fallbacks β a new RPC can be assumed to
exist on the daemon side. Backward compatibility DOES matter for
`rpc/manager/` and `rpc/agent/`, where the cluster side is upgraded
independently of the client.
### Helm Chart
The traffic-manager Helm chart is in `charts/telepresence-oss/`.
## Debugging and Troubleshooting
### Log Files
There are three log files:
- `connector.log` - Output from user daemon: traffic-manager interaction, intercepts, port forwards
- `daemon.log` - Output from root daemon: networking changes on your workstation
- `cli.log` - Output from the command line interface
Locations:
- macOS: `~/Library/Logs/telepresence/`
- Linux: `~/.cache/telepresence/logs/`
- Windows: `%USERPROFILE%\AppData\Local\logs`
Logs rotate daily. Use `tail -F <filename>` to watch rotating logs seamlessly.
### Debugging Early-Initialization Errors
If daemons fail during early initialization before logfiles are set up, run them directly to see stderr output. The `--address` flag is mandatory:
```bash
# Run user daemon directly
telepresence userd --logfile - --address :8083
# Run root daemon directly (requires sudo)
sudo telepresence rootd --logfile - --address :8084
```
### Profiling the Daemons
Enable [pprof](https://pkg.go.dev/net/http/pprof) profiling:
```bash
telepresence quit -s
telepresence connect --userd-profiling-port 6060 --rootd-profiling-port 6061
# Then browse http://localhost:6060/debug/pprof/
```
### Dumping Goroutine Stacks
Send SIGQUIT to a daemon to dump goroutine stacks to its log file. On Windows, use profiling instead.
### RBAC Testing
To test with limited RBAC privileges:
```bash
kubectl apply -f k8s/client_rbac.yaml
kubectl get sa telepresence-test-developer -o "jsonpath={.secrets[0].name}"
# Get the token from the secret and configure kubectl
kubectl get secret <secret-name> -o "jsonpath={.data.token}" | base64 --decode
kubectl config set-credentials telepresence-test-developer --token <token>
kubectl config use-context telepresence-test-developer
```
## Releases
To create a release, set `TELEPRESENCE_VERSION` and run `make prepare-release`. This creates two annotated tags (`vX.Y.Z` and `rpc/vX.Y.Z`) and a commit updating go.mod references. Pushing the tags and branch triggers the release workflow.
**Important:** A tag push publishes the release and cannot be taken back. Never push the tags directly after `make prepare-release`. Push only the branch, open a PR for it, and follow `/ship-release` (`.claude/skills/ship-release`), which drives the release PR's CI (including the required `regression` gate), creates the docs PR in the telepresence.io repository, and pushes the tags only after everything is green. The command blocks below show the mechanics, not the order.
```bash
# Test release (marked as pre-release, not promoted to latest)
export TELEPRESENCE_VERSION=v2.27.0-test.0
make prepare-release
git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION
# Release candidate
export TELEPRESENCE_VERSION=v2.27.0-rc.0
make prepare-release
git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION
# GA release (becomes "latest", updates Homebrew)
export TELEPRESENCE_VERSION=v2.27.0
make prepare-release
git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION
```
Version formats:
- `vX.Y.Z-test.N` - Test release (pre-release)
- `vX.Y.Z-rc.N` - Release candidate (pre-release)
- `vX.Y.Z` - GA release (marked as latest, triggers Homebrew update)
### Changelog
When adding entries to `CHANGELOG.yml` for an upcoming release:
- Use `date: (TBD)` for unreleased versions
- The `make prepare-release` command will set the actual date when `TELEPRESENCE_VERSION` is a GA version (e.g., `v2.27.0`)
- After modifying `CHANGELOG.yml`, run `make docs-files` to regenerate documentation
### Documentation Website
The documentation website at [telepresence.io](https://telepresence.io) is managed in the [telepresenceio/telepresence.io](https://github.com/telepresenceio/telepresence.io) repository. When creating a GA release, update the website by running `make generate-version` in that repository with:
- `DOCS_BRANCH` - Branch in this repository containing the docs (e.g., `release/v2`)
- `DOCS_VERSION` - Major.minor version to generate or update (e.g., `2.27`)
See the telepresence.io repository for full instructions.
### macOS Installer Signing and Notarization
The macOS `.pkg` installers are signed and notarized to pass Gatekeeper verification. The signing process uses a protected GitHub Environment to secure the signing credentials.
#### Environment Setup
The `build-macos-pkg` job uses the `macos-signing` environment, which must be configured in the repository settings:
1. Go to https://github.com/telepresenceio/telepresence/settings/environments
2. Create an environment named `macos-signing`
3. Enable "Required reviewers" and add authorized personnel
4. Optionally restrict deployment branches to `release/*`
5. Add the following secrets to the environment (not repository-level):
| Secret Name | Description |
|-------------|-------------|
| `MACOS_CERTIFICATE_P12` | Base64-encoded P12 file containing both Application and Developer ID Installer certificates |
| `MACOS_CERTIFICATE_PASSWORD` | Password for the P12 file |
| `MACOS_SIGN_APPLICATION` | Developer ID Application certificate name (e.g., `Developer ID Application: Your Name (TEAMID)`) |
| `MACOS_SIGN_INSTALLER` | Developer ID Installer certificate name (e.g., `Developer ID Installer: Your Name (TEAMID)`) |
| `MACOS_NOTARIZE_APPLE_ID` | Apple ID email for notarization |
| `MACOS_NOTARIZE_TEAM_ID` | Apple Developer Team ID |
| `MACOS_NOTARIZE_PASSWORD` | App-specific password for notarization |
#### Release Workflow
When a release tag is pushed:
1. All platform binaries (Linux, Windows, macOS) are built immediately
2. Linux `.deb`/`.rpm` and Windows `.exe` installers are built
3. The release is published with all binaries and Linux/Windows installers
4. The `build-macos-pkg` job waits for approval from a required reviewer
5. Once approved, signed `.pkg` installers are built and added to the release
This design ensures:
- **Emergency releases can proceed** without the signing approver being available (all binaries and Linux/Windows installers are released)
- **Signing credentials are protected** by requiring explicit approval before they are exposed
- **Signed packages are added later** when the approver reviews and approves the job
If the environment is not configured or never approved, the release will contain macOS standalone binaries but not `.pkg` installers.
#### Obtaining the Certificates
You need an [Apple Developer Program](https://developer.apple.com/programs/) membership ($99/year) to obtain signing certificates.
1. **Create certificates in Apple Developer Portal:**
- Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list)
- Click the + button to create a new certificate
- Create **Developer ID Application** certificate (for signing binaries)
- Create **Developer ID Installer** certificate (for signing .pkg files)
- Download both certificates and double-click to install in Keychain Access
2. **Find your Team ID:**
- Go to [Membership Details](https://developer.apple.com/account#MembershipDetailsCard)
- Copy the Team ID (10-character alphanumeric string)
- Set as `MACOS_NOTARIZE_TEAM_ID`
3. **Find the certificate names:**
- Open Keychain Access and look under "My Certificates"
- The names will be like:
- `Developer ID Application: Your Name (TEAMID)` β `MACOS_SIGN_APPLICATION`
- `Developer ID Installer: Your Name (TEAMID)` β `MACOS_SIGN_INSTALLER`
- You can also list them with: `security find-identity -v -p codesigning`
4. **Export certificates to P12:**
```bash
# Export each certificate from Keychain Access:
# - Right-click certificate β Export
# - Choose .p12 format
# - Set a strong password (will be MACOS_CERTIFICATE_PASSWORD)
# If you have both in separate .p12 files, you can import them together
# or export them together from Keychain Access by selecting both
# Base64-encode for GitHub secrets:
base64 -i certificates.p12 | pbcopy
# Paste as MACOS_CERTIFICATE_P12
```
5. **Create app-specific password for notarization:**
- Go to [appleid.apple.com](https://appleid.apple.com/) β Sign-In and Security β App-Specific Passwords
- Generate a new password with a descriptive name (e.g., "GitHub Actions Notarization")
- Copy the generated password β `MACOS_NOTARIZE_PASSWORD`
- Use your Apple ID email β `MACOS_NOTARIZE_APPLE_ID`
#### Testing Locally
To test signing locally before configuring GitHub secrets:
```bash
# Set environment variables
export MACOS_SIGN_APPLICATION="Developer ID Application: Your Name (TEAMID)"
export MACOS_SIGN_INSTALLER="Developer ID Installer: Your Name (TEAMID)"
export MACOS_NOTARIZE_APPLE_ID="[email protected]"
export MACOS_NOTARIZE_TEAM_ID="ABCD123456"
export MACOS_NOTARIZE_PASSWORD="xxxx-xxxx-xxxx-xxxx"
# Build the signed and notarized package
cd build-aux/pkg-installer
VERSION=2.26.0 ./build-pkg.sh
# Verify the signature
pkgutil --check-signature ../../build-output/Telepresence.pkg
spctl --assess --type install ../../build-output/Telepresence.pkg
```
Description: Add a new entry to CHANGELOG.yml under the current unreleased version (or create the version block if needed), then regenerate documentation. Use when the user says things like "add a changelog entry", "log this fix in the changelog", or "/changelog-entry".
---
name: changelog-entry
description: Add a new entry to CHANGELOG.yml under the current unreleased version (or create the version block if needed), then regenerate documentation. Use when the user says things like "add a changelog entry", "log this fix in the changelog", or "/changelog-entry".
---
# changelog-entry
Adds an entry to `CHANGELOG.yml` following the schema documented in the file header, then regenerates the derived documentation.
## Inputs to gather (in order)
1. **type** β one of `bugfix`, `feature`, `security`, `change`. If the user describes the change but does not pick a type, infer it:
- "fixes/resolves/closes a bug" β `bugfix`
- "adds support for / introduces / new" β `feature`
- "CVE / vulnerability / hardens" β `security`
- anything else affecting behavior β `change`
2. **title** β short (β€80 chars), sentence-cased, no trailing period.
3. **body** β 2-3 sentences. **This field is HTML, not markdown.** Use `<code>...</code>` for code, `<a href="...">...</a>` for links. Prefer YAML's `>-` folded scalar so line wrapping doesn't leak literal newlines.
4. **docs** *(optional)* β path to a docs page under `docs/` if the entry deserves a "Learn more" link.
5. **image** *(optional)* β path under the `release-notes` directory if there's a visual.
## Steps
1. Read the top of `CHANGELOG.yml`. The first `items:` entry is the current/upcoming version.
2. If it has `date: (TBD)`, append the new entry to its `notes:` array.
3. If the top item is already dated (a shipped release), insert a NEW `- version: <next>` block above it with `date: (TBD)` and the single new note. Ask the user for the next version number β do not invent it.
4. Match existing indentation exactly (2 spaces). YAML is whitespace-sensitive.
5. After saving, run `make docs-files` to regenerate `docs/release-notes.md`, `docs/release-notes.mdx`, and `docs/variables.yml`. (The PostToolUse hook in `.claude/settings.json` will also try to do this; running it explicitly here makes the success/failure visible.)
6. Show the user the diff: `git diff CHANGELOG.yml docs/release-notes.md docs/release-notes.mdx docs/variables.yml`.
## Schema reference (from CHANGELOG.yml header)
```yaml
items:
- version: 2.28.0
date: (TBD) # or YYYY-MM-DD
notes:
- type: bugfix # bugfix | feature | security | change
title: Short title
body: >-
Two or three sentences describing the change and why it
is noteworthy. This is HTML.
docs: optional/path
image: optional/path
```
## Things to avoid
- Do not edit `docs/release-notes.md`, `docs/release-notes.mdx`, or `docs/variables.yml` directly β they are generated.
- Do not include markdown syntax in `body`; it is rendered as HTML.
- Do not set `date:` to a concrete date for upcoming versions; `make prepare-release` does that automatically for GA versions.
Description: Create the local release commit and tags by setting TELEPRESENCE_VERSION and running make prepare-release. Stops at the local commit+tags - pushing is the ship-release skill's job. Use when the user explicitly asks to prepare a release, RC, or test build. User-only.
π User-Invoked Only
---
name: prepare-release
description: Create the local release commit and tags by setting TELEPRESENCE_VERSION and running make prepare-release. Stops at the local commit+tags - pushing is the ship-release skill's job. Use when the user explicitly asks to prepare a release, RC, or test build. User-only.
disable-model-invocation: true
---
# prepare-release
Wraps the `make prepare-release` step so the local-tag-creation portion of a release is one explicit user action, not a chain of remembered commands. Stops at local tags; the `ship-release` skill takes over from there.
This is **user-only** by design (`disable-model-invocation: true`). The tags this skill creates will eventually drive a public release, so creating them must be an explicit user decision β never a side-effect of Claude inferring intent.
## Confirm before doing anything
Ask the user explicitly:
1. **Version string** (`TELEPRESENCE_VERSION`) β must be one of:
- `vX.Y.Z-test.N` β pre-release, no Homebrew, no "latest"
- `vX.Y.Z-rc.N` β pre-release, no Homebrew, no "latest"
- `vX.Y.Z` β GA, marked latest, triggers Homebrew update
2. **Branch** β should be a release branch (typically `release/v2`). Refuse to proceed from `main`-style branches.
3. **Working tree** β must be clean. Run `git status`; if there are uncommitted or untracked files relevant to the build, stop.
4. **CHANGELOG.yml status** β the top entry should have version matching `TELEPRESENCE_VERSION` (without the leading `v`). If it's still `date: (TBD)`, that's expected: `make prepare-release` sets the date for GA versions.
Show all four checks to the user before running anything. Wait for explicit "go".
## Steps
```bash
export TELEPRESENCE_VERSION=vX.Y.Z[-suffix.N]
make prepare-release
```
This creates:
- An annotated tag `vX.Y.Z[-suffix.N]`
- An annotated tag `rpc/vX.Y.Z[-suffix.N]`
- A commit that bumps go.mod references inside the repo
Verify with:
```bash
git log -1 --stat
git tag --points-at HEAD
```
## Next: hand off to `ship-release`
This skill stops here, with the local commit and the two annotated tags. **Do not push anything.** To carry the release through CI, the docs PR, the Releases workflow, and the PR merges, invoke the `ship-release` skill (after pushing the branch and opening a PR on it β that's a manual handoff step the user does between the two skills).
## Refuse to
- Push anything (branch, commit, or tags). That's `ship-release`'s job.
- Skip `make prepare-release` and just tag manually. The make target updates go.mod references; manual tagging skips that and ships a broken module.
- Re-run `make prepare-release` on top of a previous attempt without first cleaning up the leftover tags. If `git tag --points-at HEAD` already lists the target tag, stop and report β the user has to decide whether to delete it.
Description: Run, scope, or debug telepresence regression tests under regression_test/ β the integration-level suite. Use when the user wants to run an area, suite, or single test, debug a failure, or says "/regression-tests". Runs `go test ./regression_test` scoped with -run, in the background, writing to a log file so heavy output stays out of context.
---
name: regression-tests
description: Run, scope, or debug telepresence regression tests under regression_test/ β the integration-level suite. Use when the user wants to run an area, suite, or single test, debug a failure, or says "/regression-tests". Runs `go test ./regression_test` scoped with -run, in the background, writing to a log file so heavy output stays out of context.
---
# regression-tests
Runs the telepresence regression suite from the main conversation, where this
harness's shell-env quirks are known.
## Background to assume
- Tests live under `regression_test/` and need a working k8s cluster (kind /
minikube / Docker Desktop) plus images it can reach. For a local cluster set
`RTEST_REGISTRY=local` and LOAD the images into it rather than pushing.
- `regression_test/README.md` is the reference: fixture-engine rules, the
RTEST_* table, catalogs, labels, coverage. Read it before debugging a
fixture problem.
- **The shell environment always wins; there is no config file.** That means a
stale `export` in the user's shell silently changes a run.
## Scoping: plain `go test -run`
Areas are ordinary Go tests, suites and methods are subtests, so one `-run`
expression selects at any depth:
```
go test ./regression_test -run '^TestIntercept$'
go test ./regression_test -run '^TestIntercept$/^HeaderFilter$'
go test ./regression_test -run '^TestIntercept$/^HeaderFilter$/^Test_PathPrefix$'
```
There is no `TEST_SUITE`/`TEST_NAME` indirection and no make-argument dance:
inline `VAR=value` prefixes work in this harness, so run `go test` directly
and keep `make check-regression` for the full unscoped suite.
Always pass `-count=1` (results must never come from the test cache) and a
`-timeout` that fits the scope: minutes for one suite, `-timeout=100m` for a
full run.
## The run command
```
TELEPRESENCE_REGISTRY=local TELEPRESENCE_VERSION=<version> \
RTEST_CONTEXT=<context> RTEST_TEARDOWN=1 \
go test -count=1 -timeout=30m -run '^TestArea$/^Suite$' ./regression_test \
> /tmp/rtest-suite.log 2>&1
```
- `RTEST_CONTEXT` pins the kube context. Pin it explicitly whenever the
machine has more than one cluster β the default is the kubeconfig's current
context, which is one `kubectx` away from being the wrong cluster.
- `RTEST_TEARDOWN=1` destroys the run's resources at the end. Without it, dev
mode keeps namespaces, the release, and the connection for the next run to
adopt, which is what makes a scoped rerun take seconds.
- `RTEST_FRESH=1` ignores adoptable resources (use when a previous run left
something suspect). CI implies fresh + teardown.
- `RTEST_LABELS` / `RTEST_SKIP_LABELS` select on `compat-core`, `slow`,
`stress`, `flaky-retry`.
## CRITICAL: a stale TELEPRESENCE_VERSION silently poisons the build
The version under test is read from the binary itself, and `make build` stamps
it from `TELEPRESENCE_VERSION`. If the user's shell exports an old value, the
binary gets that version, the manager image tag no longer matches, and the run
fails at image pull with a version that appears nowhere in your command.
Always pass `TELEPRESENCE_VERSION` explicitly to BOTH `make build` and the
`go test` invocation, with the same value.
## Rebuild before running
The suite runs the prebuilt binary plus the cluster-side images, so rebuild
whatever changed:
- client-side Go (`pkg/`, `cmd/telepresence`) **and any `charts/` change**:
`make build` β the chart is go:embedded in the client binary, so a
chart-only edit without a rebuild silently installs the OLD chart.
- manager / agent (`cmd/traffic`, `charts/`): `make load-images` (or
`make load-tel2-image`), so the cluster gets the new image.
- `--docker` tests: `make client-image` β the daemon container runs on the
workstation, so it only needs to exist locally.
`RTEST_REGISTRY=local` makes the manager use `pullPolicy=Never`. Reserve
`make push-images` and a real registry for a remote cluster.
## Keep heavy output out of context
This runs in the main thread, so do NOT Read or `tail` the whole log.
1. Launch with `run_in_background: true`, redirecting to a fresh path.
2. On completion, read only the summary:
`grep -E 'passed,|^ok|^--- FAIL|^FAIL' /tmp/rtest-suite.log | tail -5`
The runner's own last line is
`[rtest] run <stamp>: N passed, N failed, N skipped (N fixture actions)`.
3. For a failure, the per-test artifacts are under
`build-output/rtest/logs/<stamp>/<TestPath>/` (cli.log, daemon logs), and
`manifest.json` records every test's outcome and labels.
## Leftover state
- `make rtest-clean` removes everything the framework created, in the cluster
and locally.
- A daemon from a killed run: `telepresence quit -s`.
- One rtest run per cluster at a time β resource names are stable by design,
which is what makes adoption work.
- A leftover manager from unrelated work ("traffic-manager in namespace X
already manages namespace Y") blocks an unrestricted install: find it with
`kubectl get secret -A -l owner=helm`, and remember the chart also leaves
cluster-scoped resources (webhook config, ClusterRole/Binding) that
namespace deletion does not remove.
## Workflow
1. **Identify** the area/suite: areas are directories under
`regression_test/suites/`, and each suite is an `rt.Suite` type registered
in its `init()`.
2. **Decide the rebuild** (see above) and run it with an explicit
`TELEPRESENCE_VERSION`.
3. **Run scoped**, in the background, to a fresh log path.
4. **Summarize:** the command, pass/fail/skip counts, failing test names, the
smallest excerpt explaining each failure, and the next concrete action.
## Don't
- Don't run the full suite unscoped without explicit user instruction β it is
roughly an hour serial; `make check-regression SHARD=1|2|3` runs a third of
it (the shard/area mapping lives in build-aux/main.mk).
- Don't run `go test -list` or a deliberately non-matching `-run` to "check
what exists": the harness provisions real cluster resources before selection,
so it costs a full setup cycle. Grep the suite files instead.
- Don't `make clobber` or destroy local images without asking.
- Don't edit generated files: `docs/reference/cli/**`, `DEPENDENCIES.md`,
`DEPENDENCY_LICENSES.md`, `docs/release-notes*`.
Description: Drive a Telepresence release from a prepared branch all the way through CI, docs, the Releases workflow, and PR merges. Assumes `make prepare-release` has already been run locally and the branch with that commit was pushed and a PR opened. Use when the user says "ship the release", or "complete the release". User-only.
π User-Invoked Only
---
name: ship-release
description: Drive a Telepresence release from a prepared branch all the way through CI, docs, the Releases workflow, and PR merges. Assumes `make prepare-release` has already been run locally and the branch with that commit was pushed and a PR opened. Use when the user says "ship the release", or "complete the release". User-only.
disable-model-invocation: true
---
# ship-release
End-to-end driver for releasing Telepresence. Picks up where `prepare-release` left off and carries the change through:
1. Telepresence release PR (CI green + `regression` green)
2. Docs PR in `../telepresence.io`
3. Tag push, Releases workflow, merge of both PRs.
This is **user-only** (`disable-model-invocation: true`). A release is publicly visible and partially irreversible (tags push to GitHub, Homebrew updates for GA). Claude must never invoke this on its own.
## Preconditions to verify before doing anything
Run each check and stop with a clear message if any fails:
1. **CWD is the telepresence repo.** `git rev-parse --show-toplevel` ends in `telepresence`.
2. **`make prepare-release` has been run.** The current HEAD must carry both `vX.Y.Z` and `rpc/vX.Y.Z` annotated tags locally:
```
git tag --points-at HEAD | sort
```
Two entries expected. If the tag list is empty or missing the `rpc/` peer, stop β the user needs to run `make prepare-release` first.
3. **Branch is pushed.** Capture `tp_branch=$(git branch --show-current)`, then:
```
git ls-remote --exit-code origin "refs/heads/$tp_branch"
```
If this fails, stop and tell the user to push the branch first.
4. **PR exists.** `gh pr view "$tp_branch" --json number,state,url,headRefName`. If no PR, stop.
5. **Sibling docs repo present.** `test -d ../telepresence.io && test -d ../telepresence.io/.git`. If not, stop.
Capture once and reuse throughout:
- `tp_branch` β name of the prepare-release branch.
- `tp_version` β pick the non-`rpc/` tag from `git tag --points-at HEAD` (e.g. `v2.28.0`).
- `docs_version` β `echo "$tp_version" | sed -E 's/^v([0-9]+\.[0-9]+).*/\1/'` (e.g. `2.28`).
- `pr_number` β from `gh pr view`.
## Phase 1 β Drive the Telepresence release PR
### 1.1 Verify branch and PR (already done in preconditions)
### 1.2 Wait for all checks except `regression` to be green
Use:
```
gh pr checks "$tp_branch" --json name,state,conclusion
```
Filter out the rows whose name is exactly `regression` or `node_agent_docker_runtime` (neither has been triggered yet β the label triggers them). Match the name exactly: `regression_compat` is a different job, and it only runs behind the `compatibility test` label. For every remaining row:
- `state == "COMPLETED"` and `conclusion == "SUCCESS"` β green
- `conclusion β {"FAILURE","CANCELLED","TIMED_OUT","ACTION_REQUIRED"}` β **stop**. Report the failing check name and a short excerpt from `gh run view <run-id> --log-failed`. Do not advance.
- Anything else (`IN_PROGRESS`, `QUEUED`, `PENDING`) β keep waiting.
**Polling cadence:** these checks (lint, unit tests, license, image-scan) typically finish in 5-15 min. Use `ScheduleWakeup` with `delaySeconds=180` while any check is still running. Do not tight-loop with sleeps.
### 1.3 Wait for `regression` to be green
`regression` starts automatically with the push (the release branch lives in
this repository); there is nothing to trigger. If it needs another attempt,
use "Re-run all jobs" on its workflow run (`gh run rerun <run-id>`).
The regression suite runs as three parallel shards (~30 min including cluster setup), summed into the single `regression` context; the node-agent job runs beside them. Use `ScheduleWakeup` with `delaySeconds` around **900**. Poll with the same `gh pr checks` query, looking at the `regression` and `node_agent_docker_runtime` rows.
- Success β continue to Phase 2.
- Failure / cancellation β **stop and report**. Pull failed-step logs with `gh run view <run-id> --log-failed`.
- Still running after ~90 minutes β tell the user and stop (workflow may be stuck).
## Phase 2 β Create the docs PR
Done in the sibling repo `../telepresence.io`. Each shell step below is a separate Bash call (no `&&` chains), and `cd` to switch repos.
### 2.1 Pull master
```
cd ../telepresence.io
git checkout master
git pull
```
### 2.2 Create branch with the same name as the telepresence PR branch
```
git checkout -b "$tp_branch"
```
If that branch already exists locally from a previous attempt, stop and ask whether to reuse, reset, or rename.
### 2.3 + 2.4 Export variables
```
export DOCS_VERSION="$docs_version" # e.g. 2.28 β note: no patch number
export DOCS_BRANCH="$tp_branch"
```
(Per CLAUDE.md, `export` in its own Bash call, then use in subsequent calls. Shell state persists between calls in a session.)
### 2.5 Generate
```
make generate-version
```
### 2.6 Verify output
```
ls versioned_docs/version-"$DOCS_VERSION"
git status
```
Expectations:
- **Minor release** (first time this `2.X` is generated): the directory `versioned_docs/version-$DOCS_VERSION/` appears as untracked.
- **Bugfix release** (directory already existed): files within it are modified.
If `versioned_docs/version-$DOCS_VERSION` is absent or `git status` shows no changes, **stop and report** β `make generate-version` did not do anything useful.
### 2.7 Build the site locally before pushing
Netlify (the `deploy/netlify`, `Pages changed`, `Header rules`, `Redirect rules`
checks) and the `Check`/`Lint` GitHub jobs all run `yarn build` (docusaurus
build). Run it locally first so a broken build is caught here, not after a
push-and-wait CI cycle:
```
# If node_modules is absent: yarn install --frozen-lockfile
yarn build
```
- Exit 0 β the production build (including the new `version-$DOCS_VERSION`)
compiled. Proceed to the PR.
- Non-zero β **stop and do not push.** Read the error; it names the offending
file and line.
**Most common failure: MDX parse error in `release-notes.mdx`.** A `.mdx` file
is JSX, so literal `{` / `}` **inside an HTML element** like
`<code>{cmd, stdout}</code>` are parsed as JS expressions and fail with
`Could not parse expression with acorn`. (Braces inside Markdown backtick
spans β `` `{tcp|udp}` `` β are safe.) These release-notes files are generated,
so fix the **source**, not the generated copy:
1. In the telepresence repo, edit the offending `CHANGELOG.yml` entry to remove
the literal braces (rephrase, e.g. `<code>cmd</code>/<code>stdout</code>`, or
move the snippet into a backtick span), then `make docs-files`.
2. Commit + push that fix to the release branch (it belongs in the release PR).
3. Back in the docs repo, re-run `make generate-version` to re-pull the fixed
docs, then `yarn build` again before continuing.
### 2.8 Create the PR
```
git add versioned_docs/version-"$DOCS_VERSION" versioned_sidebars docusaurus.config.js versions.json
# (Add only the files that actually changed β git status will tell you which of the
# above moved; for a fresh minor you'll likely see all of them, for a bugfix only some.)
git commit -s -S -m "Generate docs for telepresence $tp_version"
git push -u origin "$tp_branch"
gh pr create --base master --head "$tp_branch" \
--title "Generate docs for telepresence $tp_version" \
--body "Generated with \`make generate-version\` DOCS_VERSION=$docs_version DOCS_BRANCH=$tp_branch."
```
Capture `docs_pr_number` from the `gh pr create` output URL.
Do not include "Co-Authored-By" or "Generated with" trailers in the commit message or the PR body (per global preferences).
### 2.9 Monitor the docs PR checks
```
gh pr checks "$tp_branch" --json name,state,conclusion
```
Same polling rules as Phase 1.2. If anything fails, **stop and report**.
## Phase 3 β Release
`cd` back to the telepresence repo for step 3.1 and 3.3a.
### 3.1 Push the release tags
```
git push origin "$tp_version" "rpc/$tp_version"
```
This triggers the **Releases** workflow (`.github/workflows/release.yaml`). The release PR is still unmerged at this point β that is intentional. Merging now would create a new commit and move the branch tip away from the tagged commit.
### 3.2 Monitor the Releases workflow
```
gh run list --workflow=release.yaml --limit 1 --json databaseId,status,conclusion,url
gh run view <id> --json jobs
```
The workflow requires manual approval of a protected GitHub environment (`macos-signing`) containing secrets for macOS signing. Wait for this up to **24 hours**. Poll with `ScheduleWakeup` at `delaySeconds=1800` (or longer when overnight). Surface the workflow URL early so the user can chase the approver.
- If the workflow completes successfully β continue to 3.3.
- If a job other than `build-macos-pkg` fails β **stop and report**.
- If `build-macos-pkg` itself is never approved within 24h β tell the user; per CLAUDE.md the release still ships without `.pkg` installers, and the user can decide whether to proceed to 3.3 anyway.
### 3.3 Merge both PRs β **GA versions only**
**For pre-release versions (`-test.N`, `-rc.N`): skip this step entirely and
stop here.** Both PRs stay open until the GA release ships: the release
branch accumulates the rc and GA prepare-release commits and merges once,
after GA, and the docs PR must not publish the new version's docs on
telepresence.io before GA exists (regenerate it from the GA branch before
merging). The rc's GitHub pre-release and its tags are the only public
artifacts of a pre-release ship.
For a GA version: order does not matter. Both must use a **merge commit**
(CLAUDE.md: never squash, never rebase).
```
# telepresence PR (in telepresence repo)
gh pr merge "$tp_branch" --merge
# docs PR (in ../telepresence.io)
cd ../telepresence.io
gh pr merge "$tp_branch" --merge
```
Verify each merged: `gh pr view "$tp_branch" --json state` should report `MERGED`.
## Long-wait strategy
- Anything under 5 min β don't sleep; just poll once.
- 5-30 min waits (Phase 1.2 non-regression checks) β `ScheduleWakeup` with `delaySeconds=180`.
- 30-60 min waits (Phase 1.3 `regression`) β `ScheduleWakeup` with `delaySeconds=1200`.
- Hours-to-overnight (Phase 3.2 macOS signing approval) β `ScheduleWakeup` with `delaySeconds=1800` or longer.
Each wake-up: re-fetch state, decide green/red/still-waiting, schedule the next wake or advance.
## What "stop and report" means
- Do not advance to the next numbered step.
- Surface: the step that failed, the check/run name(s), the run URL(s), and a short excerpt from `gh run view <id> --log-failed`.
- Do not retry automatically. Wait for the user to direct.
- Do not delete branches, force-push, or close PRs. The user decides what to do.
## What this skill must NEVER do
- Run `make prepare-release` itself β that's a separate skill and a separate decision.
- Push tags before all required PR checks are green (Phase 1 must complete first).
- Merge the release PR or the docs PR for a pre-release (`-test.*`/`-rc.*`) version β both stay open until GA (see 3.3).
- Merge PRs as squash or rebase β both repos require merge commits.
- Trigger `regression` by any means other than the push itself or a re-run of its workflow run.
- Approve the `macos-signing` environment programmatically β that requires a human reviewer.
- Force-push or delete the release branch.
Description: Use when reviewing changes to any .proto file under rpc/ or to the Go bindings generated from them. Verifies wire-level backward compatibility, that 'make protoc' has been run, that protolint passes, and that both sides of each affected RPC are updated. Surfaces incompatibilities that would break older clients, older traffic-managers, or older traffic-agents talking to a new peer.
---
name: proto-rpc-reviewer
description: Use when reviewing changes to any .proto file under rpc/ or to the Go bindings generated from them. Verifies wire-level backward compatibility, that 'make protoc' has been run, that protolint passes, and that both sides of each affected RPC are updated. Surfaces incompatibilities that would break older clients, older traffic-managers, or older traffic-agents talking to a new peer.
tools: Read, Grep, Glob, Bash
---
You are the gRPC contract reviewer for the telepresence repository.
## Communication boundaries you must consider
The repo defines four RPC surfaces; a single proto edit can ripple across them:
| Boundary | Proto package |
|----------------------------------------|----------------------|
| client/userd β traffic-manager | `rpc/manager/` |
| client β user daemon | `rpc/connector/` |
| client β root daemon | `rpc/daemon/` |
| traffic-manager β traffic-agent | `rpc/agent/` |
| auth | `rpc/authenticator/` |
| teleroute (docker network driver) | `rpc/teleroute/` |
| shared types | `rpc/common/` |
Each daemon ships independently: an older client may talk to a newer traffic-manager, a newer traffic-manager may inject an older traffic-agent (mismatched manifest), and a newer agent may run alongside an older sidecar in another pod. Wire compatibility is therefore mandatory, not optional.
## Checks you must run
1. **Wire compatibility:**
- Field numbers must never be reused or repurposed.
- Field types must not change (e.g., int32 β int64 silently corrupts).
- Enum values must not be renumbered; only appended.
- `optional` and `repeated` are part of the wire contract; do not flip.
- Removing a field requires `reserved` to lock the number/name.
2. **Generated code is in sync:** Confirm `make protoc` has been run β check that .pb.go files in the same package are touched in the same change. If not, flag and recommend running it.
3. **Lint:** Confirm `protolint` would pass against the configured rules in `.protolint.yaml` (line length 120, ENUM_FIELD_NAMES_PREFIX disabled). Spot-check naming conventions for fields (snake_case in proto, mapped to PascalCase in Go).
4. **Both sides updated:** For every RPC method added or changed, locate the server implementation (usually under `cmd/traffic/cmd/manager/`, `cmd/traffic/cmd/agent/`, `pkg/client/userd/`, or `pkg/client/rootd/`) AND the call site(s). If only one side is touched, flag it.
5. **Compat shims:** If the change adds a field that older peers don't know about, confirm the server tolerates its absence and the client treats nil/zero correctly. Reject any change that requires a synchronized upgrade of both sides.
## Reporting format
Return a punch list, not prose. For each finding:
- **Severity:** Blocker / Risk / Nit
- **Where:** file:line
- **Why:** one sentence
- **Fix:** one sentence
End with a one-line verdict: "Safe to merge", "Needs follow-up", or "Blocked".
## What NOT to do
- Do not edit any files. You are a reviewer.
- Do not run `make protoc` yourself; report whether it appears to have been run and let the caller decide.
- Do not chase code-style nits unrelated to the proto/RPC contract.
name = "integration_test_runner"
description = "Use when the user wants to run, identify, or debug integration tests in integration_test/. Locates relevant testify suites by feature keywords, decides whether images need rebuilding, runs a focused subset, and returns a tight pass/fail summary."
developer_instructions = """
You are the integration-test specialist for the telepresence repository.
Context to assume:
- Integration tests live under integration_test/ and use testify suites. They require a working Kubernetes cluster, typically Docker Desktop's built-in cluster or kind.
- The test harness lives in integration_test/itest/.
- Single-test invocation: go test ./integration_test/... -v -testify.m=Test_InterceptDetailedOutput
- Suite-scoped invocation: TEST_SUITE='^WorkloadConfiguration$' go test ./integration_test/... -v
- Tests rely on built images: telepresence client and tel2 cluster-side.
- If the user changed Go code in pkg/, cmd/, rpc/, or charts/, images probably need rebuilding with make build client-image tel2-image.
- When DEV_CLIENT_REGISTRY=local, tests use pullPolicy=Never.
- Required environment is documented in CLAUDE.md under Integration Test Environment Variables.
Workflow:
1. Identify the right suite/test. Search integration_test/ to map the user's feature description to suite or test names. Suites typically follow <feature>_test.go and embed itest.NamespacePair or similar; tests are method names starting with Test_.
2. Decide if a rebuild is needed. Check git status and git diff --name-only HEAD, or the last commit, for changes under pkg/, cmd/, rpc/, or charts/. If yes, propose the rebuild commands; only run them if the user agreed or pre-authorized.
3. Run the focused subset. Prefer narrow -testify.m= patterns over running everything. Always pass -v. If the user wants a full suite, prefer TEST_SUITE='^<Suite>$' over running the whole package.
4. Summarize. Return commands run, pass/fail counts, failing test names, and the smallest log excerpt that explains each failure.
Do not:
- Run the full integration suite without explicit user instruction.
- Run make clobber or anything that destroys local images without asking.
- Modify test files unless the user asked for that specifically.
- Edit generated files under docs/reference/cli/**, DEPENDENCIES.md, DEPENDENCY_LICENSES.md, docs/release-notes*, or docs/variables.yml.
Reporting format:
Keep the response to 200-400 words. Lead with a pass/fail headline, then failing test names, then per-failure excerpts. End with the next concrete action.
"""
name = "proto_rpc_reviewer"
description = "Use when reviewing changes to .proto files under rpc/ or generated Go bindings. Verifies wire-level backward compatibility, generated-code sync, protolint expectations, and that both sides of affected RPCs were updated."
sandbox_mode = "read-only"
developer_instructions = """
You are the gRPC contract reviewer for the telepresence repository.
Communication boundaries to consider:
- client/userd <-> traffic-manager: rpc/manager/
- client <-> user daemon: rpc/connector/
- client <-> root daemon: rpc/daemon/
- traffic-manager <-> traffic-agent: rpc/agent/
- auth: rpc/authenticator/
- teleroute docker network driver: rpc/teleroute/
- shared types: rpc/common/
Each daemon ships independently. Older clients may talk to newer traffic-managers, newer traffic-managers may inject older traffic-agents, and newer agents may run alongside older sidecars in other pods. Wire compatibility is mandatory.
Checks to run:
1. Wire compatibility: field numbers are never reused or repurposed; field types do not change; enum values are only appended; optional/repeated are not flipped; removed fields reserve number and name.
2. Generated code is in sync: confirm make protoc appears to have been run by checking .pb.go files in the same package are touched in the same change.
3. Lint: confirm protolint expectations from .protolint.yaml would pass, including line length 120 and ENUM_FIELD_NAMES_PREFIX disabled.
4. Both sides updated: for every added or changed RPC method, locate server implementation and call sites. If only one side is touched, flag it.
5. Compat shims: if the change adds fields older peers do not know about, confirm servers tolerate absence and clients treat nil/zero correctly. Reject changes that require synchronized upgrades.
Reporting format:
Return a punch list, not prose. For each finding include:
- Severity: Blocker / Risk / Nit
- Where: file:line
- Why: one sentence
- Fix: one sentence
End with one verdict: Safe to merge, Needs follow-up, or Blocked.
Do not edit files, run make protoc, or chase style nits unrelated to the proto/RPC contract.
"""