This guide covers the day-to-day workflow for contributing to Alcove: building, testing, adding features, and understanding the codebase conventions.
alcove/
cmd/
alcove/ CLI client
bridge/ Bridge controller (REST API, scheduler, dashboard)
gate/ Gate auth proxy sidecar
shim/ Dev container execution sidecar (GET /healthz, POST /exec)
skiff-init/ Skiff ephemeral worker init process
internal/
auth/ Authentication backends (Authenticator interface)
bridge/ Bridge internals
api.go REST API handlers and route registration
config.go Bridge configuration (env var parsing)
credentials.go Credential storage and encryption
dispatcher.go Task dispatching to runtime
scheduler.go Background task scheduling
migrations/ Embedded SQL migration files
gate/ Gate proxy, scope enforcement, domain allowlist
hail/ NATS messaging helpers
ledger/ PostgreSQL session store helpers
runtime/ Runtime interface and implementations (podman, kubernetes)
types.go Shared types (Session, Scope, TranscriptEvent, etc.)
build/
alcove-credential-helper Git credential helper binary (installed in Skiff image)
Containerfile.* Multi-stage container image definitions
deploy/ Kubernetes/OpenShift manifests
docs/ Documentation
web/ Dashboard static files
bin/ Build output (gitignored)
All build and test operations use make. Run make help to see all targets.
make buildThis compiles all binaries (bridge, gate, skiff-init, alcove,
etc.) into the bin/ directory. Version information is injected via -ldflags
from git describe. The shim binary (cmd/shim) is compiled as part of
the dev container image build (make build-dev) and baked in via s6-overlay.
make build-imagesBuilds three container images with podman:
localhost/alcove-bridge:<version>localhost/alcove-gate:<version>localhost/alcove-skiff-base:<version>
A .containerignore file ensures only the necessary files are sent to the
build context (~2 MB instead of the full repo), dramatically speeding up
container builds.
Pre-built tooling base image: The Skiff image includes heavy tooling (language servers, CLIs). To avoid rebuilding this layer every time, build the tooling base image separately:
make build-tooling # Heavy base image (~minutes, only when tools change)
make build-images # Fast overlay builds (~30s with pre-built tooling)Parallel builds: Build all three images concurrently:
make -j3 build-imagesmake build-devBuilds localhost/alcove-dev:<version> from build/Containerfile.dev. This is
an all-in-one dev container image that includes PostgreSQL 16, NATS, Go 1.25,
the shim binary, and s6-overlay for process supervision. Agent definitions that
declare dev_container.image can reference this image (or any project-specific
image built on top of it). The shim, PostgreSQL, and NATS are managed as s6
supervised services with proper startup dependencies.
Claude Code runs with --bare inside Skiff, which disables native CLAUDE.md
file discovery. To restore project context, skiff-init reads CLAUDE.md from
cloned repos after checkout and prepends the content to the agent prompt. For
single-repo sessions it reads /workspace/CLAUDE.md; for multi-repo sessions
it reads /workspace/<name>/CLAUDE.md from each repo. This means project
instructions (coding conventions, build commands, dev container usage patterns)
are automatically available to agents without duplicating them in agent
definition prompts.
make testRuns go test ./... across the entire module.
make test-networkValidates the dual-network setup by checking that the internal and external podman networks are configured correctly.
make lintRuns go vet and staticcheck. Install staticcheck first:
go install honnef.co/go/tools/cmd/staticcheck@latestThere are two ways to run Alcove locally. Both require podman.
Before your first make up, set up .dev-credentials.yaml -- the single
source of truth for dev credentials (LLM provider and GitHub PAT):
cp .dev-credentials.yaml.example .dev-credentials.yaml
# Edit .dev-credentials.yaml — uncomment one LLM provider block and fill in values.
# Optionally add your GitHub PAT (or it will fall back to `gh auth token`).This file is gitignored. When you run make up (or make watch), make dev-config merges the LLM settings from .dev-credentials.yaml into
alcove.yaml (the Bridge infrastructure config), configuring the system LLM.
To also create API-level credentials (LLM + GitHub) in the database for
session execution, run the dev-up skill, which reads .dev-credentials.yaml
and creates the appropriate credentials via the Bridge API.
You only need to create this file once. It persists across make down /
make up cycles.
For day-to-day Go code changes, use hot-reload:
make up # First-time: build binaries + start PostgreSQL/NATS + run Bridge (~12s)
make watch # Hot-reload: auto-rebuilds Bridge on .go file changesmake watch uses Air to watch for Go file changes and automatically rebuilds
and restarts Bridge. This is the fastest feedback loop for Bridge development.
The database uses a named PostgreSQL volume that persists across restarts, so you do not need to re-seed credentials every time you restart.
make up # Build binaries + start PostgreSQL/NATS + run Bridge
make down # Stop everything
make logs # Tail logs from all containers
make dev-reset # Stop + remove database volumes (clean slate)make up starts PostgreSQL (Ledger), NATS (Hail), and Bridge on a
dual-network pattern: alcove-internal (an --internal network with no
external access) and alcove-external (for Gate egress). Skiff containers are
attached only to the internal network; Gate bridges both networks. The Bridge
process gets access to the host's podman socket so it can create Skiff+Gate
containers.
The dashboard is available at http://localhost:8080. Log in with admin /
admin and change the password after first login.
If you need a completely fresh database (new migrations, corrupted state):
make dev-reset # Nuke database + volumes
make up # Fresh startStart only NATS and PostgreSQL in containers, then run Bridge as a local
process. This is an alternative to make up if you want more control over
Bridge startup flags.
make dev-infra # start PostgreSQL + NATS only
# In another terminal:
make build
LEDGER_DATABASE_URL="postgres://alcove:alcove@localhost:5432/alcove?sslmode=disable" \
HAIL_URL="nats://localhost:4222" \
RUNTIME=podman \
./bin/bridgeBridge reads these environment variables:
| Variable | Purpose | Example |
|---|---|---|
LEDGER_DATABASE_URL |
PostgreSQL connection string | postgres://alcove:alcove@localhost:5432/alcove?sslmode=disable |
HAIL_URL |
NATS server URL | nats://localhost:4222 |
RUNTIME |
Container runtime to use | podman or kubernetes |
SKIFF_IMAGE |
Skiff container image | ghcr.io/bmbouter/alcove-skiff-base:latest |
GATE_IMAGE |
Gate container image | ghcr.io/bmbouter/alcove-gate:latest |
ALCOVE_WEB_DIR |
Path to dashboard static files | /web or ./web |
ALCOVE_NETWORK |
Podman internal network name | alcove-internal |
ALCOVE_EXTERNAL_NETWORK |
Podman external network for Gate egress | alcove-external |
AUTH_BACKEND |
Authentication backend | memory or postgres |
ALCOVE_DATABASE_ENCRYPTION_KEY |
Encryption key for stored credentials | (secret string) |
ALCOVE_DEBUG |
Keep worker containers after exit for debugging | true or false |
BRIDGE_URL |
URL where Bridge is reachable by Skiff/Gate | http://host.containers.internal:8080 |
SKIFF_HAIL_URL |
NATS URL as seen from inside Skiff containers | nats://host.containers.internal:4222 |
AGENT_REPO_SYNC_INTERVAL |
How often Bridge syncs agent definitions from repos | 15m (default) |
Set these as environment variables before running Bridge.
Migrations live in internal/bridge/migrations/ as embedded SQL files. They
are applied automatically on Bridge startup.
-
Determine the next version number by looking at existing files:
ls internal/bridge/migrations/ # 001_initial_schema.sql ... 029_catalog_items.sql -
Create a new file with the next numeric prefix and a descriptive name:
touch internal/bridge/migrations/002_add_task_labels.sql
-
Write the SQL. Use
IF NOT EXISTSfor safety. Example:-- 002_add_task_labels.sql -- Adds a labels column to the sessions table for task categorization. ALTER TABLE sessions ADD COLUMN IF NOT EXISTS labels JSONB DEFAULT '{}'; CREATE INDEX IF NOT EXISTS idx_sessions_labels ON sessions USING GIN (labels);
-
That is it. The migration runner embeds the
migrations/directory at compile time via//go:embed. When Bridge starts, it:- Acquires a PostgreSQL advisory lock to prevent concurrent migration runs
- Reads the
schema_migrationstable to find which versions are applied - Sorts migration files by numeric prefix
- Runs each pending migration in its own transaction
- Records the version in
schema_migrations
NNN_short_description.sql
NNNis a zero-padded integer (001, 002, 003, ...)- The description uses underscores, lowercase
- The numeric prefix is parsed by splitting on the first
_and converting to an integer, so001and1both resolve to version 1
- Each migration runs in a single transaction. If it fails, the transaction rolls back and Bridge will not start.
- Migrations are idempotent by convention (use
IF NOT EXISTS,ADD COLUMN IF NOT EXISTS, etc.). - Never modify an already-applied migration. Always create a new one.
The Bridge REST API is implemented in internal/bridge/api.go using the
standard library net/http package. There are no frameworks.
Every handler follows this structure:
func (a *API) handleMyResource(w http.ResponseWriter, r *http.Request) {
// 1. Check HTTP method.
if r.Method != http.MethodGet {
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
// 2. Parse and validate input (path params, query params, or JSON body).
var req MyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
return
}
// 3. Perform the operation (database query, dispatch, etc.).
result, err := a.doSomething(r.Context(), req)
if err != nil {
log.Printf("error: doing something: %v", err)
respondError(w, http.StatusInternalServerError, "failed to do something")
return
}
// 4. Respond with JSON.
respondJSON(w, http.StatusOK, result)
}For resources that support multiple HTTP methods on the same path, use a
switch on r.Method:
func (a *API) handleMyResource(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// list or get
case http.MethodPost:
// create
default:
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}Register the handler in the RegisterRoutes method:
func (a *API) RegisterRoutes(mux *http.ServeMux) {
// ... existing routes ...
mux.HandleFunc("/api/v1/myresource", a.handleMyResource)
mux.HandleFunc("/api/v1/myresource/", a.handleMyResourceByID)
}Routes follow the pattern /api/v1/<resource> for collection endpoints and
/api/v1/<resource>/ (trailing slash) for individual resource endpoints. The
trailing-slash handler parses the ID from the URL path manually:
func (a *API) handleMyResourceByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/v1/myresource/")
if id == "" {
respondError(w, http.StatusBadRequest, "id required")
return
}
// ...
}Use the two provided helpers for all responses:
respondJSON(w, http.StatusOK, data) // success with JSON body
respondError(w, http.StatusBadRequest, "msg") // error with {"error": "msg"}API routes use the X-Alcove-Team header to scope requests to a team. The
team middleware extracts this header on every authenticated request and makes
the team ID available to handlers. If the header is missing, the request is
scoped to the user's personal team. The middleware validates that the
authenticated user is a member of the requested team (returning 403 if not).
API routes are protected by the auth middleware. The authenticated username is
available via r.Header.Get("X-Alcove-User"). These paths are public:
/api/v1/auth/login/api/v1/health/api/v1/internal/*(internal service-to-service calls)
Additionally, POST requests to paths ending in /transcript, /status, or
/proxy-log are exempt from user authentication. These are session ingestion
paths used by Skiff and Gate to report data back to Bridge. They are
authenticated via session tokens instead of user tokens. See
isSessionIngestionPath() in internal/auth/auth.go.
The authentication system is defined by the Authenticator interface in
internal/auth/auth.go:
type Authenticator interface {
Authenticate(username, password string) (string, error)
ValidateToken(token string) (string, bool)
InvalidateToken(token string)
}To add a new backend (for example, LDAP or OIDC):
-
Create a new file in
internal/auth/, e.g.,ldap.go. -
Define a struct that implements the
Authenticatorinterface:type LDAPAuthenticator struct { serverURL string baseDN string // ... } func (l *LDAPAuthenticator) Authenticate(username, password string) (string, error) { // Bind to LDAP, verify credentials. // On success, generate and store a session token. token, err := generateToken() if err != nil { return "", err } // Store token -> username mapping with expiry. return token, nil } func (l *LDAPAuthenticator) ValidateToken(token string) (string, bool) { // Look up token, check expiry, return username. return username, true } func (l *LDAPAuthenticator) InvalidateToken(token string) { // Remove the token from the store. }
-
Wire it into Bridge startup based on configuration (e.g., an
AUTH_BACKENDenvironment variable).
If the backend also supports user management, implement the UserManager
interface:
type UserManager interface {
CreateUser(ctx context.Context, username, password string) error
DeleteUser(ctx context.Context, username string) error
ListUsers(ctx context.Context) ([]UserInfo, error)
ChangePassword(ctx context.Context, username, newPassword string) error
}Use the provided HashPassword and VerifyPassword functions from the auth
package. They use argon2id with these parameters: 64 MB memory, 3 iterations,
parallelism 4, 32-byte key.
The Runtime interface in internal/runtime/runtime.go abstracts over
container runtimes. There are two implementations:
- PodmanRuntime (
podman.go) -- creates Skiff and Gate as separate containers on dual podman networks (--internalfor isolation) - KubernetesRuntime (
kubernetes.go) -- creates a k8s Job with Gate as a native sidecar (init container withrestartPolicy: Always) and Skiff as the main container. Uses a staticalcove-allow-internalNetworkPolicy for egress restriction (per-task NetworkPolicy is disabled due to OVN-Kubernetes DNS issues).
Set RUNTIME=podman or RUNTIME=kubernetes to select the backend.
The Kubernetes runtime uses direct client-go API calls (no operator or CRDs). Key design points:
- Jobs with native sidecars: Gate runs as an init container with
restartPolicy: Always, which makes it a native sidecar that starts before and stops after the main Skiff container. Gate and Skiff share the pod's network namespace, so proxy env vars point tolocalhost:8443. - NetworkPolicy: per-task NetworkPolicy creation is disabled due to
OVN-Kubernetes DNS resolution failures. A static
alcove-allow-internalpolicy provides egress restriction instead. Service hostnames are resolved to IPs at dispatch time to bypass DNS issues in task pods. - OpenShift compatible: security contexts use
restricted-v2SCC (non-root, drop all capabilities,seccompProfile: RuntimeDefault). - Minimal RBAC: Bridge needs create/get/list/delete on Jobs and NetworkPolicies in its namespace.
- Namespace detection: uses
ALCOVE_NAMESPACEenv var, then in-cluster service account namespace, then defaults toalcove. - Direct outbound support: When
direct_outbound: trueis set, the pod gets analcove.dev/direct-outbound: "true"label andHTTP_PROXY/HTTPS_PROXYenv vars are omitted. A static NetworkPolicy namedalcove-allow-direct-outboundmust be deployed in the namespace to grant full egress to pods with that label. This NetworkPolicy is not created by Bridge -- it must be provisioned by the cluster administrator.
To test with Kubernetes locally, use kind or minikube:
RUNTIME=kubernetes KUBECONFIG=~/.kube/config ./bin/bridgetype Runtime interface {
RunTask(ctx context.Context, spec TaskSpec) (TaskHandle, error)
CancelTask(ctx context.Context, handle TaskHandle) error
TaskStatus(ctx context.Context, handle TaskHandle) (string, error)
EnsureService(ctx context.Context, spec ServiceSpec) error
StopService(ctx context.Context, name string) error
CreateVolume(ctx context.Context, name string) (string, error)
Info(ctx context.Context) (RuntimeInfo, error)
}To add a new runtime:
- Create a new file in
internal/runtime/. - Implement all seven methods.
RunTaskmust start both Skiff and Gate with shared networking and proxy configuration. - Wire it into Bridge startup based on the
RUNTIMEenvironment variable.
Agent definitions are YAML files in .alcove/agents/*.yml within an agent repo:
name: run-tests
prompt: |
Run the full test suite and fix any failures.
repos:
- url: https://github.com/org/myproject.git
provider: anthropic
model: claude-sonnet-4-20250514
timeout: 1800
budget_usd: 5.0
profiles:
- read-only-github
tools:
- github
schedule: "0 2 * * *"All fields except name and prompt are optional. The schedule field uses
standard 5-field cron syntax. Schedules, security profiles, and tools are
YAML-only -- they cannot be created, updated, or deleted through the API.
The API provides read-only access to synced data.
The workflow engine supports a workflow graph with bounded cycles and two step types: agent steps (Skiff pods) and bridge steps (deterministic Bridge actions). This moves infrastructure concerns like PR creation, CI polling, and merging out of LLM prompts and into reliable Bridge code.
| File | Purpose |
|---|---|
internal/bridge/bridge_actions.go |
Bridge action implementations (create-pr, await-ci, merge-pr). Each action is a function that takes step inputs and returns outputs. |
internal/bridge/depends.go |
Depends expression parser and evaluator. Parses boolean expressions with &&, ||, parentheses, and .Succeeded/.Failed conditions. |
internal/bridge/dispatcher.go |
Workflow step dispatch logic, iteration tracking, cycle detection |
internal/bridge/migrations/028_workflow_graph_v2.sql |
Schema for workflow_run_steps iteration tracking and step type/action columns |
- When a workflow runs, the dispatcher evaluates each step's
dependsexpression against the current state of all steps. - For
type: agentsteps, the dispatcher creates a Skiff pod (existing behavior). - For
type: bridgesteps, the dispatcher calls the corresponding bridge action function inline -- no container is created. - After each step completes, the dispatcher re-evaluates all pending steps.
Steps in cycles can become eligible again if their
max_iterationshas not been exhausted. - The
workflow_run_stepstable tracksiteration_countper step to enforcemax_iterationslimits.
To test agent repo syncing locally:
- Create a test git repo with a
.alcove/agents/directory containing YAML agent files. - Push it to a Git host or use a local bare repo.
- Register the repo via the API or dashboard.
- Wait for the sync interval (default 15 minutes, configurable via
AGENT_REPO_SYNC_INTERVAL) or trigger a manual sync viaPOST /api/v1/agent-definitions/syncor the "Sync Now" button in the dashboard. - Check the dashboard or
GET /api/v1/agent-definitionsto verify the agents appear.
Note: Catalog items (plugins, agents, LSPs, MCPs) are seeded from data embedded at compile time, so they are available immediately on Bridge startup without cloning catalog source repos. Only custom agent repo definitions require the sync interval.
Gate intercepts CONNECT tunnels to service domains (GitHub, GitLab, Jira)
via MITM TLS. An ephemeral CA is generated per session by Bridge; the CA cert
is injected into Skiff's trust store (SSL_CERT_FILE, NODE_EXTRA_CA_CERTS).
Gate generates leaf certs signed by this CA, terminates TLS tunnels, inspects
plaintext requests, enforces operation-level scope (e.g., allowing
create_pr_draft but blocking merge_pr), injects real SCM credentials,
and re-encrypts to the upstream API. Tools like gh and glab work natively
through HTTP_PROXY without per-tool API URL env vars.
See internal/gate/ for the proxy implementation and
docs/design/gate-scm-authorization.md for the full design.
All resources are owned by teams rather than individual users. The database
schema reflects this with a team_id column on all resource tables (sessions,
credentials, security profiles, agent definitions, schedules, tools, agent
repos). The migration 027_teams.sql creates three tables:
| Table | Columns | Purpose |
|---|---|---|
teams |
id, name, type, created_at, updated_at |
Team registry. Type is personal or shared. |
team_members |
team_id, username, joined_at |
Maps users to teams. |
team_settings |
team_id, key, value |
Per-team settings (e.g., agent repos). |
The same migration renames owner to team_id on existing resource tables.
A personal team is auto-created for each user on signup. Personal teams cannot be deleted, renamed, or have members added. Users can create additional shared teams and invite others. All team members have equal permissions.
The X-Alcove-Team header is required on all authenticated API requests. The
team middleware validates membership and injects the team context. If the
header is omitted, the user's personal team is used as the default.
The catalog uses per-item granularity for enabling and disabling individual
items within sources. Migration 029_catalog_items.sql creates two tables:
| Table | Columns | Purpose |
|---|---|---|
catalog_items |
id, source_id, slug, name, description, item_type, definition, source_file, synced_at |
Individual items within catalog sources (plugins, agents, LSPs, MCPs). |
team_catalog_items |
team_id, source_id, item_slug, enabled, enabled_at |
Per-team enable/disable state for individual catalog items. |
The catalog introspection flow:
- Bridge loads catalog sources from
catalog.json(embedded at compile time). - Each source contains items discovered during sync.
- Teams enable or disable individual items via the API.
- At dispatch time, Bridge resolves enabled items into skill repos for Skiff.
- At sync time, Bridge validates workflow agent references against enabled items.
The test-teams.sh script in scripts/ exercises team CRUD, membership
management, and team-scoped resource isolation against a running Bridge
instance.
The PodmanRuntime tests in internal/runtime/podman_test.go demonstrate how
to test code that shells out to external commands (like podman) without
requiring the actual binary. This technique is from
https://npf.io/2015/06/testing-exec-command/.
The pattern has three parts:
1. A fake exec function factory:
func fakeExecCommand(t *testing.T, stdout string, exitCode int) (
func(ctx context.Context, name string, args ...string) *exec.Cmd,
*[][]string,
) {
var calls [][]string
fn := func(ctx context.Context, name string, args ...string) *exec.Cmd {
calls = append(calls, append([]string{name}, args...))
cs := []string{"-test.run=TestHelperProcess", "--", name}
cs = append(cs, args...)
cmd := exec.CommandContext(ctx, os.Args[0], cs...)
cmd.Env = []string{
"GO_WANT_HELPER_PROCESS=1",
fmt.Sprintf("GO_HELPER_STDOUT=%s", stdout),
fmt.Sprintf("GO_HELPER_EXIT_CODE=%d", exitCode),
}
return cmd
}
return fn, &calls
}2. The helper process test (not a real test):
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
fmt.Fprint(os.Stdout, os.Getenv("GO_HELPER_STDOUT"))
exitCode := 0
if code := os.Getenv("GO_HELPER_EXIT_CODE"); code != "" && code != "0" {
exitCode = 1
}
os.Exit(exitCode)
}3. Usage in tests:
func TestRunTask_CommandConstruction(t *testing.T) {
execFn, calls := fakeExecCommand(t, "container-id-123\n", 0)
p := &PodmanRuntime{
PodmanBin: "podman",
execCommand: execFn,
}
spec := TaskSpec{TaskID: "task-1", Image: "skiff:latest", GateImage: "gate:latest"}
handle, err := p.RunTask(context.Background(), spec)
// ... assertions on handle and *calls ...
}For commands that need different responses on successive calls, use
fakeExecCommandMulti with a slice of fakeResponse structs:
responses := []fakeResponse{
{stdout: "[]", exitCode: 0}, // first call returns empty
{stdout: "cid\n", exitCode: 0}, // second call succeeds
}
execFn, calls := fakeExecCommandMulti(t, responses)This lets you verify the exact command-line arguments passed to podman
without running real containers.
All Containerfiles use multi-stage builds:
- Build stage:
golang:1.25-- compiles the Go binary withCGO_ENABLED=0for a static binary - Runtime stage: varies per component (see table below)
Example from build/Containerfile.bridge:
FROM docker.io/library/golang:1.25 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/bridge ./cmd/bridge
FROM registry.access.redhat.com/ubi9/ubi:latest
COPY --from=builder /out/bridge /usr/local/bin/bridge
RUN dnf install -y podman && \
useradd -r -u 1001 -s /sbin/nologin alcove && \
dnf clean all
USER 1001
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/bridge"]Local development images use this tag format:
localhost/alcove-<component>:<version>
Examples:
localhost/alcove-bridge:devlocalhost/alcove-gate:devlocalhost/alcove-skiff-base:dev
The version defaults to the output of git describe --tags --always --dirty,
or dev if not in a git repository.
Pre-built images are available at ghcr.io/bmbouter/alcove-<component>.
Pulling images:
make pull # Pull latest version
make pull VERSION=v0.1.0 # Pull specific versionPushing images (maintainers):
Requires a GitHub PAT with write:packages scope.
# Login (one-time per session)
GHCR_TOKEN=ghp_xxx GHCR_USER=youruser make login-registry
# Build and push
make push VERSION=v0.1.0Release process:
git tag -a v0.1.0 -m "Release v0.1.0"
make push VERSION=v0.1.0
git push origin v0.1.0Image naming:
| Image | Registry Path |
|---|---|
| Bridge | ghcr.io/bmbouter/alcove-bridge:<version> |
| Gate | ghcr.io/bmbouter/alcove-gate:<version> |
| Skiff | ghcr.io/bmbouter/alcove-skiff-base:<version> |
Each push also updates the latest tag.
All Containerfiles live in build/:
| File | Image | Notes |
|---|---|---|
Containerfile.bridge |
alcove-bridge |
Base: ubi9/ubi (needs podman for spawning Skiff+Gate) |
Containerfile.gate |
alcove-gate |
Base: ubi9-minimal (lightweight proxy binary) |
Containerfile.skiff-base |
alcove-skiff-base |
Base: ubi9/ubi (Claude Code worker environment; includes gh, glab, alcove-credential-helper, and git config forcing HTTPS) |
Containerfile.dev |
alcove-dev |
Base: golang:1.25 (all-in-one dev container with PostgreSQL 16, NATS, shim binary, s6-overlay; built with make build-dev) |