How to Switch to Self-Managed Tekton Pipelines
If you followed How to Deploy Your First Service with Zero-Config CI/CD, you have a Service building on one of Planton's platform tracks — dockerfile or buildpacks. The platform builds your image, renders your kustomize overlays, and deploys — with no pipeline file in your repository.
That works until you need a step the platform track does not have: SonarQube analysis before every production deploy, a unit-test stage, a build tool like Bazel. A self-managed pipeline is the answer: plain Tekton YAML in your repository, which Planton compiles at dispatch into a self-contained run and executes on every commit — while it keeps handling webhook triggering, credentials, status streaming, and deployment.
What You Will Learn
- Write a Tekton pipeline that clones your repository, runs a SonarQube scan, builds the image, and renders your kustomize overlays
- Reuse Planton's catalog tasks by name and add a Task of your own beside the pipeline
- Validate the pipeline locally with the same compiler that runs at dispatch
- Switch your Service to the pipeline with one field
- Gate a step on the trigger — for example, run it only on tag releases
Prerequisites
- A working Service deployed through Planton on a platform track (see the tutorial above)
- A Dockerfile in your repository (the catalog's
buildkit-daemonlesstask builds from it) - A SonarCloud account with a project for your repository, and a Kubernetes Secret holding its token in your build cluster's pipelines namespace (coordinate with your platform administrator)
- The
plantonCLI installed and authenticated (planton auth login) - Basic familiarity with Tekton's Pipeline and Task concepts
Step 1: Write the pipeline
Create .planton/pipeline.yaml under your service's project root. It declares the platform's parameter contract, the one workspace the platform binds (source), and four tasks. Three come from Planton's catalog by plain name; one — the SonarQube scan — lives beside the pipeline in your repository.
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: build-with-sonar
spec:
params:
- name: git-url
type: string
- name: git-revision
type: string
- name: git-branch
type: string
default: ""
- name: project-root
type: string
default: "."
- name: sparse-checkout-directories
type: string
default: ""
- name: image-name
type: string
- name: dockerfile-config-map-name
type: string
- name: build-context
type: string
default: "."
- name: dockerfile-path
type: string
default: "Dockerfile"
- name: kustomize-manifests-config-map-name
type: string
- name: kustomize-base-directory
type: string
default: "_kustomize"
- name: owner-identifier-label-key
type: string
default: ""
- name: owner-identifier-label-value
type: string
default: ""
- name: sonar-project-key
type: string
workspaces:
- name: source
tasks:
- name: git-checkout
taskRef:
name: git-clone
params:
- name: url
value: "$(params.git-url)"
- name: revision
value: "$(params.git-revision)"
- name: deleteExisting
value: "true"
- name: sparseCheckoutDirectories
value: "$(params.sparse-checkout-directories)"
- name: gitInitImage
value: "ghcr.io/tektoncd/github.com/tektoncd/pipeline/cmd/git-init:v0.45.0@sha256:8ab0f58d8381b0b71f5b2bae1f63522989d739e3154d8cab1bacfa0ef5317214"
workspaces:
- name: output
workspace: source
- name: sonar-analysis
runAfter: [git-checkout]
taskRef:
name: sonar-scan
params:
- name: project-key
value: "$(params.sonar-project-key)"
- name: project-root
value: "$(params.project-root)"
workspaces:
- name: source
workspace: source
- name: build-image
runAfter: [sonar-analysis]
taskRef:
name: buildkit-daemonless
params:
- name: image
value: "$(params.image-name)"
- name: contextDir
value: "$(params.build-context)"
- name: dockerfilePath
value: "$(params.dockerfile-path)"
- name: cache
value: "true"
- name: dockerfile-config-map-namespace
value: "$(context.pipelineRun.namespace)"
- name: dockerfile-config-map-name
value: "$(params.dockerfile-config-map-name)"
- name: owner-identifier-label-key
value: "$(params.owner-identifier-label-key)"
- name: owner-identifier-label-value
value: "$(params.owner-identifier-label-value)"
workspaces:
- name: source
workspace: source
- name: kustomize-build
runAfter: [git-checkout]
taskRef:
name: kustomize-build
params:
- name: config-map-name
value: "$(params.kustomize-manifests-config-map-name)"
- name: config-map-namespace
value: "$(context.pipelineRun.namespace)"
- name: project-root
value: "$(params.project-root)"
- name: kustomize-base-directory
value: "$(params.kustomize-base-directory)"
- name: owner-identifier-label-key
value: "$(params.owner-identifier-label-key)"
- name: owner-identifier-label-value
value: "$(params.owner-identifier-label-value)"
workspaces:
- name: source
workspace: source
Read each catalog task's own parameters before wiring it — their definitions are plain YAML in the open-source repository under cicd/tekton/tasks/. Never guess a parameter name; the compiler will tell you, but the file tells you first.
Step 2: Add your own Task beside the pipeline
sonar-scan is not in the catalog, so define it yourself in .planton/tasks/sonar-scan.yaml — an ordinary Tekton Task. The compiler discovers every Task in the tasks/ directory beside your pipeline file and resolves taskRef: {name: sonar-scan} to it.
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: sonar-scan
spec:
params:
- name: project-key
type: string
- name: project-root
type: string
default: "."
workspaces:
- name: source
steps:
- name: scan
image: sonarsource/sonar-scanner-cli:11
workingDir: $(workspaces.source.path)/$(params.project-root)
env:
- name: SONAR_TOKEN
valueFrom:
secretKeyRef:
name: sonar-token
key: token
script: |
sonar-scanner -Dsonar.projectKey=$(params.project-key) -Dsonar.host.url=https://sonarcloud.io
Pin the image by digest before you rely on it (crane digest sonarsource/sonar-scanner-cli:11, then image: sonarsource/sonar-scanner-cli:11@sha256:...) so a moved tag can never change your build. The token is a runtime secret reference — the compiler refuses literal secret values in a definition.
Step 3: Validate before you push
planton service pipeline validate .planton/pipeline.yaml \
--param sonar-project-key=acme_storefront -o json
The same compiler that runs at dispatch runs here: it discovers sonar-scan beside the file, inlines git-clone, buildkit-daemonless, and kustomize-build from the catalog, and checks the parameter contract in both directions. Every problem comes back in one pass as a named verdict — undeclared_param, unresolved_task_ref, unbindable_workspace — each naming the field to fix. Exit code 0 means it compiles clean.
Step 4: Switch the Service to your pipeline
Replace the platform builder with tektonPipeline and supply your own parameter:
spec:
build:
tektonPipeline:
params:
sonar-project-key: acme_storefront
registry: your-registry-connection
imageRepositoryPath: acme/storefront
The default file location is .planton/pipeline.yaml; set tektonPipeline.yamlFile to keep it elsewhere. Apply the manifest:
planton apply -f service.yaml
Step 5: Push and watch
git add .planton/pipeline.yaml .planton/tasks/sonar-scan.yaml service.yaml
git commit -m "Build with SonarQube analysis"
git push origin main
The push starts a run through the same webhook as before. Watch it:
planton service runs storefront
planton service follow <run-id>
planton service logs <run-id>
You will see git-checkout, sonar-analysis, build-image, and kustomize-build. If the scan fails its quality gate, build-image never runs and the run fails with the scanner's own output. The run record carries the exact compiled pipeline that executed (spec.resolved_pipeline), so a rerun replays it byte-for-byte.
Common Patterns
Run a step only on releases. Declare the optional git-tag parameter — the platform supplies it only because you declared it — and gate the task with Tekton's when:
params:
- name: git-tag
type: string
default: ""
tasks:
- name: sbom
when:
- input: "$(params.git-tag)"
operator: notin
values: [""]
taskRef:
name: generate-sbom
pull-request-number and trigger-type (webhook, manual, rerun) work the same way.
Keep everything in one file. A pipeline file may carry its Tasks as additional ----separated documents — one Pipeline, any number of Tasks — exactly as kubectl apply -f accepts.
Override a catalog task. A Task in your tasks/ directory with the same name as a catalog task wins; the run record shows yours.
Share a task across services. Publish it once as a TektonTask record (planton apply -f task.yaml); every service in the organization resolves it by name.
What to Do Next
- Self-Managed Pipelines — the full contract, every compile verdict, and its fix
- Pipelines — the run model, stages, and gates
- How to Configure Branch Deployments and Tag Releases