···110110its own doc per provider:
111111112112* [Buildkite](docs/buildkite.md)
113113+* [Tekton](docs/tekton.md)
+128
docs/tekton.md
···11+# Tekton
22+33+The Tekton provider runs only in Kubernetes. Tack receives Tangled
44+pipeline triggers, creates a Tekton `PipelineRun` for an existing
55+in-cluster `Pipeline`, watches that `PipelineRun`, and publishes
66+`sh.tangled.pipeline.status` records back to Tangled.
77+88+Tekton Triggers are intentionally not used. Tack already performs the
99+event-to-run translation, and Tekton's native execution object is the
1010+`PipelineRun`.
1111+1212+## Required cluster setup
1313+1414+* Tekton Pipelines is installed in the cluster.
1515+* Tack is deployed inside the same cluster.
1616+* The target Tekton `Pipeline` objects already exist in the namespace
1717+ tack is configured to use.
1818+* Tack's Kubernetes service account has RBAC to:
1919+ * create, get, list, and watch `tekton.dev` `pipelineruns`
2020+ * get, list, and watch `tekton.dev` `taskruns`
2121+ * get and list pods
2222+ * get pod logs via `pods/log`
2323+2424+Example RBAC:
2525+2626+```yaml
2727+apiVersion: rbac.authorization.k8s.io/v1
2828+kind: Role
2929+metadata:
3030+ name: tack-tekton
3131+ namespace: ci
3232+rules:
3333+ - apiGroups: ["tekton.dev"]
3434+ resources: ["pipelineruns"]
3535+ verbs: ["create", "get", "list", "watch"]
3636+ - apiGroups: ["tekton.dev"]
3737+ resources: ["taskruns"]
3838+ verbs: ["get", "list", "watch"]
3939+ - apiGroups: [""]
4040+ resources: ["pods"]
4141+ verbs: ["get", "list"]
4242+ - apiGroups: [""]
4343+ resources: ["pods/log"]
4444+ verbs: ["get"]
4545+```
4646+4747+## Configure Tack
4848+4949+| Env var | Description |
5050+| ------------------------ | --------------------------------------------------------- |
5151+| `TACK_TEKTON_ENABLED` | Set to `1` to enable the Tekton provider |
5252+| `TACK_TEKTON_NAMESPACE` | Namespace for created `PipelineRun`s (default `default`) |
5353+5454+The provider uses Kubernetes in-cluster service account credentials.
5555+It will not run from a local kubeconfig.
5656+5757+## Naming
5858+5959+There are three separate names:
6060+6161+* Tack workflow name: the Tangled workflow filename/name, e.g. `ci.yml`.
6262+ This remains the Tangled-facing workflow identity in status records.
6363+* Tekton `Pipeline` name: the existing in-cluster pipeline definition,
6464+ e.g. `repo-ci`. This is written to `spec.pipelineRef.name`.
6565+* Tekton `PipelineRun` name: generated by tack per trigger/workflow,
6666+ e.g. `tack-ci-yml-<short-hash>`. This is the concrete execution
6767+ object tack watches and stores.
6868+6969+## Workflow YAML
7070+7171+Only the provider and target pipeline are required:
7272+7373+```yaml
7474+tack:
7575+ tekton:
7676+ pipeline: repo-ci
7777+```
7878+7979+Optional fields:
8080+8181+```yaml
8282+tack:
8383+ tekton:
8484+ pipeline: repo-ci
8585+ service_account: pipeline-runner
8686+ params:
8787+ image: example/app
8888+```
8989+9090+`params` are forwarded as string Tekton params. Tack also stores the
9191+knot, pipeline rkey, workflow name, actor DID, commit, and branch as
9292+`PipelineRun` annotations, so operators can inspect the Kubernetes
9393+object and connect it back to the Tangled trigger.
9494+9595+## Example Pipeline
9696+9797+```yaml
9898+apiVersion: tekton.dev/v1
9999+kind: Pipeline
100100+metadata:
101101+ name: repo-ci
102102+ namespace: ci
103103+spec:
104104+ params:
105105+ - name: image
106106+ type: string
107107+ tasks:
108108+ - name: test
109109+ taskSpec:
110110+ params:
111111+ - name: image
112112+ type: string
113113+ steps:
114114+ - name: test
115115+ image: golang:1.25
116116+ script: |
117117+ set -eu
118118+ echo "building $(params.image)"
119119+ go test ./...
120120+ workspaces: []
121121+ params:
122122+ - name: image
123123+ value: $(params.image)
124124+```
125125+126126+Detailed CI behavior belongs in the in-cluster `Pipeline`. The Tangled
127127+workflow YAML should stay small: select `tekton`, pick the target
128128+pipeline, and pass only the small set of params that pipeline expects.
···526526 PipelineURI string
527527}
528528529529+// TektonRunRef is the persisted link from a Tangled workflow tuple
530530+// to the in-cluster PipelineRun tack created for it. The tuple is the
531531+// user-facing identity the appview knows; namespace/name/uid are the
532532+// Kubernetes identity needed for status watching and log lookup.
533533+type TektonRunRef struct {
534534+ Knot string
535535+ PipelineRkey string
536536+ Workflow string
537537+ Namespace string
538538+ PipelineRunName string
539539+ PipelineRunUID string
540540+ PipelineName string
541541+ PipelineURI string
542542+}
543543+529544// InsertBuildkiteBuild records that a Buildkite build was created on
530545// behalf of the given (knot, pipelineRkey, workflow) tuple. Uses
531546// INSERT OR REPLACE so that an unlikely build-uuid collision (or a
···624639 }
625640 if err != nil {
626641 return nil, fmt.Errorf("lookup buildkite_build by tuple: %w", err)
642642+ }
643643+ return &ref, nil
644644+}
645645+646646+// InsertTektonRun records the latest PipelineRun created for a Tangled
647647+// workflow tuple. Reusing the tuple as the primary key intentionally
648648+// makes /logs resolve to the newest run for that workflow identity.
649649+func (s *store) InsertTektonRun(ctx context.Context, ref TektonRunRef) error {
650650+ now := time.Now().UTC()
651651+ _, err := s.db.ExecContext(ctx,
652652+ `INSERT INTO tekton_runs (
653653+ knot, pipeline_rkey, workflow,
654654+ namespace, pipeline_run_name, pipeline_run_uid,
655655+ pipeline_name, pipeline_uri, created_at, created_unix_ns
656656+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
657657+ ON CONFLICT(knot, pipeline_rkey, workflow) DO UPDATE SET
658658+ namespace = excluded.namespace,
659659+ pipeline_run_name = excluded.pipeline_run_name,
660660+ pipeline_run_uid = excluded.pipeline_run_uid,
661661+ pipeline_name = excluded.pipeline_name,
662662+ pipeline_uri = excluded.pipeline_uri,
663663+ created_at = excluded.created_at,
664664+ created_unix_ns = excluded.created_unix_ns`,
665665+ ref.Knot, ref.PipelineRkey, ref.Workflow,
666666+ ref.Namespace, ref.PipelineRunName, ref.PipelineRunUID,
667667+ ref.PipelineName, ref.PipelineURI, now.Format(time.RFC3339Nano), now.UnixNano(),
668668+ )
669669+ if err != nil {
670670+ return fmt.Errorf("insert tekton_run: %w", err)
671671+ }
672672+ return nil
673673+}
674674+675675+// LookupTektonRunByTuple resolves the appview's path-based identity to
676676+// the concrete PipelineRun tack created in Kubernetes.
677677+func (s *store) LookupTektonRunByTuple(ctx context.Context, knot, pipelineRkey, workflow string) (*TektonRunRef, error) {
678678+ var ref TektonRunRef
679679+ err := s.db.QueryRowContext(ctx,
680680+ `SELECT knot, pipeline_rkey, workflow,
681681+ namespace, pipeline_run_name, pipeline_run_uid,
682682+ pipeline_name, pipeline_uri
683683+ FROM tekton_runs
684684+ WHERE knot = ? AND pipeline_rkey = ? AND workflow = ?`,
685685+ knot, pipelineRkey, workflow,
686686+ ).Scan(
687687+ &ref.Knot, &ref.PipelineRkey, &ref.Workflow,
688688+ &ref.Namespace, &ref.PipelineRunName, &ref.PipelineRunUID,
689689+ &ref.PipelineName, &ref.PipelineURI,
690690+ )
691691+ if errors.Is(err, sql.ErrNoRows) {
692692+ return nil, nil
693693+ }
694694+ if err != nil {
695695+ return nil, fmt.Errorf("lookup tekton_run by tuple: %w", err)
627696 }
628697 return &ref, nil
629698}
+21
store_migrate.go
···121121);
122122CREATE INDEX IF NOT EXISTS buildkite_builds_lookup
123123 ON buildkite_builds (knot, pipeline_rkey, workflow);
124124+125125+-- Mapping from a Tangled workflow tuple to the latest Tekton
126126+-- PipelineRun tack created for it. Unlike Buildkite webhooks, Tekton
127127+-- status observation happens in-process, so the primary read path is
128128+-- /logs resolving (knot, pipeline_rkey, workflow) back to the concrete
129129+-- PipelineRun whose TaskRuns and pods hold output.
130130+CREATE TABLE IF NOT EXISTS tekton_runs (
131131+ knot TEXT NOT NULL,
132132+ pipeline_rkey TEXT NOT NULL,
133133+ workflow TEXT NOT NULL,
134134+ namespace TEXT NOT NULL,
135135+ pipeline_run_name TEXT NOT NULL,
136136+ pipeline_run_uid TEXT NOT NULL,
137137+ pipeline_name TEXT NOT NULL,
138138+ pipeline_uri TEXT NOT NULL,
139139+ created_at TEXT NOT NULL,
140140+ created_unix_ns INTEGER NOT NULL,
141141+ PRIMARY KEY (knot, pipeline_rkey, workflow)
142142+);
143143+CREATE INDEX IF NOT EXISTS tekton_runs_uid
144144+ ON tekton_runs (pipeline_run_uid);
124145`
125146126147// migrate applies the schema. Safe to call repeatedly.