Skip to main content

Retrofit Overlays

The *-service-empty-archetype tier is not a service generator. It is a platform overlay: run it against an existing repository to retrofit that repository with the platform's servicing layer, generating no project scaffolding and no domain code.

archetect render git@github.com:p6m-archetypes/golang-service-empty-archetype.git#dev /path/to/existing-project

There is one per language, and they are the tier to reach for when onboarding a legacy application:

dotnet-service-empty-archetype · golang-service-empty-archetype · java-service-empty-archetype · python-service-empty-archetype · rust-service-empty-archetype · typescript-service-empty-archetype

The Workflows Are Identical

Every empty archetype composes the same <lang>-ci-library as its full service sibling, with the same call and no options:

local ci = require("golang-ci")
ci.render(context, dest)

So the generated build.yaml and cut-tag.yaml are byte-for-byte what CI/CD Workflows documents - same triggers, same permissions, same twelve-step chain, same shared steps, same secrets and variables, same CD handoff, and the same absence of a promote.yaml. Read the per-language page for your language; none of it changes here.

What changes is the context the pipeline runs in. Four differences matter.

1. Everything Renders in Place

A full service archetype renders into a project-name/ subdirectory. An overlay passes an empty destination, so every file lands at the destination root:

TierDestination
Full service archetypelocal dest = { destination = context:get("project-name") }
Empty overlaylocal dest = {} - the repository root you pointed it at

That is what makes it a retrofit rather than a sibling checkout.

2. The Container Build Is Parameterized, Not Assumed

This is the substantive CI/CD difference. build.yaml still hardcodes dockerfile-path: .platform/docker/prd/Dockerfile, but the overlay's version of that Dockerfile cannot assume a module name, a crate, a src/ layout, or an output path - it is retrofitting somebody else's repository. So it copies the whole repo and runs the application's own build command, supplied by two extra prompts the full archetypes do not have:

PromptKeyFeeds
Build Commandbuild_commandThe RUN line in the builder stage
Runtime Artifactruntime_artifactThe path the runtime stage copies out and executes

The rendered Go Dockerfile, for example:

FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN {{ build_command }}

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/{{ runtime_artifact }} /server
EXPOSE {{ service_port }}
EXPOSE {{ management_port }}
ENTRYPOINT ["/server"]

The defaults reproduce what each language's own service archetype produces, so a greenfield-shaped repository needs no answer at all. A legacy repository overrides one line instead of rewriting a Dockerfile:

Languagebuild_command defaultruntime_artifact default
.NETdotnet publish -c Release -o /app/publish<ProjectName>.dll
GoCGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o server ./cmd/serverserver
Javamvn -q -B -DskipTests packagetarget/*.jar
Pythonuv pip install --system --no-cache .<project-name> (console script on PATH)
Rustcargo build --release<project-name> (binary in target/release/)
TypeScriptpnpm builddist/index.js
Java skips tests in the image build

The Java default is -DskipTests, because the Build step has already run mvn verify against the same commit. The container build packages; it does not re-test.

3. Nothing Is Overwritten

The overlay archetypes document, and their shared acceptance suite holds, that a retrofit leaves the application's own files alone - archetect never overwrites an existing path.

Existing CI survives, so the platform CI may not land

If the repository you are retrofitting already has a .github/workflows/build.yaml, the overlay's build workflow is not written. You keep the legacy pipeline and silently get no platform CI - no digest, no manifest dispatch, no deployment. Check .github/workflows/ after rendering, and if the platform workflow is absent, reconcile the two by hand.

The same applies to .editorconfig, .gitattributes, .gitignore, and the Dockerfiles.

4. The Guarded Build Steps Now Read the Legacy Repository

Several steps in the shared pipeline are guarded on files existing, and the per-language pages describe how those guards resolve for a freshly generated project. On a retrofit they resolve against whatever the legacy repository already contains, which flips some conclusions:

LanguageGuarded behaviorGreenfieldOn a retrofit
Pythonuv run ruff check needs [tool.ruff] in pyproject.tomlSkippedLive gate if the project already configures Ruff
Pythonuv run pytest needs [tool.pytest] or test_*.pyRunsRuns if the project has tests
TypeScriptpnpm lint needs a lint scriptSkippedLive gate if the project defines one
TypeScriptpnpm test / pnpm build need those scriptsRunSkipped if absent - a missing test script silently disables testing
Javamvn checkstyle:check needs the plugin in pom.xmlSkippedLive gate if the project already uses Checkstyle
Goinstall-protoc: auto and codegen key off proto/ and gqlgen.ymlPer protocolActivate on the project's existing files
GoToolchain version read from go.modArchetype'sThe legacy go.mod
Rustcargo fmt --check and clippy -D warnings are unguardedHard gatesHard gates against legacy code from the first run
.NETdotnet-setup defaults to SDK 8.0.xMatchesDoes not adapt - pin dotnet-version or add a global.json

The two rows worth planning around before you open the PR:

  • Rust has no escape hatch. cargo fmt --check and cargo clippy -- -D warnings run on every branch with no existence guard, so a legacy Rust codebase that is not already clean fails its first CI run outright. Either clean it or override run-format-check / run-lint in the rendered build.yaml.
  • .NET silently builds with SDK 8.0.x regardless of what the project targets. Add a global.json, or pass dotnet-version to both dotnet-setup and dotnet-build.

What Else the Overlay Renders

PathNotes
.github/workflows/build.yaml, cut-tag.yamlFrom <lang>-ci-library - identical to the full tier
.platform/kubernetes/**PlatformApplication CRD plus dev/stg/prd overlays, including resourceRequirements
.platform/docker/prd/DockerfileThe image CI publishes, parameterized as above
.platform/docker/local/DockerfileLocal development image
TiltfileGo and Rust only. The .NET, Java, Python and TypeScript overlays do not render one
.editorconfig, .gitattributes, .gitignoreOnly where the repository does not already have them

Prompt Surface

Smaller than the full tier: no author identity, no organization/solution split, no project prefix or suffix, because those exist to name code the overlay does not generate.

PromptConsumed by
Application NameIMAGE_NAME and APPLICATION_NAME in build.yaml, the PlatformApplication name, and the kubernetes/<dir>/ directory CD writes into
Solution SlugThe image path, and the {solution}-{application}-{env} namespace
Image RegistryThe image path
ProtocolWhether the manifests inject SERVER_PORT or GRPC_PORT
Service / Management PortPublished ports, the container EXPOSE lines, the readiness probe
Build Command / Runtime ArtifactThe production Dockerfile
Persistence / Cache / MessagingThe manifests' resourceRequirements only - no connection code is woven
Source ControlThe optional publish-the-repository step

Only the first three have no default, so a headless render needs just:

archetect render <source> /path/to/existing-project --headless \
-a project_name=billing-service \
-a org_solution_name=acme-payments \
-a image_registry=ghcr.io
The platform database is named <application>_db

A full service archetype names the platform-provisioned database from its identity prompts. An overlay has none, so it names it after the application - unmistakably distinct from whatever database the legacy application may already carry.