Skip to content

Commit 5856295

Browse files
committed
EPMDEDP-16758: feat: Add krci pipelinerun start command
Implement foundational verb for triggering Tekton pipelines directly from CLI with parameter and label overrides. Supports dry-run mode for manifest preview and multiple output formats (table/json/yaml). - Portal service wraps tRPC pipelineRun.start procedure with error mapping - CLI validates pipeline name, params, labels via Cobra Args validators - Server-assigned names via metadata.generateName; client reads resolved name - Comprehensive error disambiguation: pipeline-not-found, trigger-template-not-found - Unified richNotFoundError type for all portal service not-found scenarios - Includes e2e test cases and comprehensive unit test coverage Signed-off-by: Sergiy Kulanov <sergiy_kulanov@epam.com>
1 parent d0f6a5e commit 5856295

29 files changed

Lines changed: 3392 additions & 262 deletions

cmd/krci/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Package main is the entry point for the krci CLI.
21
package main
32

43
import (

docs/json-schemas.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,81 @@ Common messages:
477477
| Unknown pull request (404) | `pull request <id> not found` |
478478
| Upstream 5xx / network | `portal returned HTTP 500: <cause>` |
479479
| Invalid flag value | Flag-specific message (e.g. enum list) |
480+
481+
482+
## `krci pipelinerun start`
483+
484+
The start verb reuses the same column shape as `krci pipelinerun list`. Empty
485+
cells render as `-` in table mode and as `""` in JSON mode (matches list).
486+
487+
### Success envelope
488+
489+
```json
490+
{
491+
"schemaVersion": "1",
492+
"data": {
493+
"name": "<apiserver-assigned name, e.g. foo-build-run-x9k2p>",
494+
"status": "Pending|Running|Succeeded|Failed|Cancelled|Timeout",
495+
"project": "<codebase or empty>",
496+
"pr": "<pr number or empty>",
497+
"author": "<git author or empty>",
498+
"type": "<pipelinetype label or empty>",
499+
"started": "<RFC3339 or empty>",
500+
"duration": "<m+s or empty>"
501+
}
502+
}
503+
```
504+
505+
### Error envelope
506+
507+
```json
508+
{
509+
"schemaVersion": "1",
510+
"error": { "message": "pipeline 'ghost' not found" }
511+
}
512+
```
513+
514+
### Dry-run envelope (-o json)
515+
516+
`data` carries the rendered `PipelineRun` resource as a parsed JSON object —
517+
not a string. Default and `-o yaml` modes emit the same resource as YAML
518+
(suitable for piping to `kubectl apply -f -`).
519+
520+
```json
521+
{
522+
"schemaVersion": "1",
523+
"data": {
524+
"apiVersion": "tekton.dev/v1",
525+
"kind": "PipelineRun",
526+
"metadata": {
527+
"generateName": "foo-build-run-",
528+
"labels": { "app.edp.epam.com/codebase": "my-app" }
529+
},
530+
"spec": {
531+
"params": [ { "name": "git-revision", "value": "main" } ]
532+
}
533+
}
534+
}
535+
```
536+
537+
### Common messages
538+
539+
User-facing messages on the not-found path are synthesised CLI-side from a
540+
stable `error.reason` tag the Portal returns. The Portal deliberately does
541+
not put resource-identifying text in `error.message` (cluster-hardening
542+
policy applied uniformly to all REST routes), so the CLI builds the user
543+
message from the pipeline name it already has plus the reason it received.
544+
545+
All errors exit `1` (per the global rule at the top of this document).
546+
547+
| Condition | Message |
548+
| ------------------------------------------- | --------------------------------------------------------------------------------------------- |
549+
| Pipeline not found | `pipeline '<name>' not found` |
550+
| TriggerTemplate referenced but missing | `pipeline '<name>' references a TriggerTemplate that does not exist` |
551+
| Malformed TriggerTemplate label | `platform rejected request: pipeline '<name>' has malformed TriggerTemplate label` |
552+
| Platform admission rejection (400/422) | `platform rejected request: <HTTP status phrase>` (Portal does not echo K8s admission detail) |
553+
| RBAC denied | `permission denied` |
554+
| Portal upstream 5xx | `upstream service unavailable: <cause>` |
555+
| Duplicate / malformed `--param` / `--label` | `duplicate parameter '<k>'` / `parameter must be key=value` / `label key must not be empty` |
556+
| `--dry-run` with `-o table` | `--dry-run cannot use -o table (use -o json or -o yaml)` |
557+

docs/pipelinerun.md

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ deploy, release). Also surfaces logs and a focused failure-diagnosis view.
77

88
## Subcommands
99

10-
| Command | Purpose |
11-
|-------------------------------|------------------------------------------|
12-
| `pipelinerun list` (`ls`) | List and filter runs |
13-
| `pipelinerun get <name>` | Inspect a specific run |
10+
| Command | Purpose |
11+
|-------------------------------|-----------------------------------------------|
12+
| `pipelinerun list` (`ls`) | List and filter runs |
13+
| `pipelinerun get <name>` | Inspect a specific run |
14+
| `pipelinerun start <pipeline>` | Create a new run from a Tekton pipeline name |
1415

15-
Both accept `-o, --output` (`table` | `json`), `--logs`, and `--reason`.
16+
`list` and `get` accept `-o, --output` (`table` | `json`), `--logs`, and
17+
`--reason`. `start` has its own flag set (`--param`, `--label`, `--dry-run`,
18+
`-o`) — see below.
1619

1720
## `pipelinerun list`
1821

@@ -73,6 +76,80 @@ Project: keycloak-operator
7376

7477
Add `--logs` for full logs or `--reason` for focused failure diagnosis.
7578

79+
## `pipelinerun start`
80+
81+
Create a new run of a Tekton pipeline by name. The pipeline name is the
82+
required argument; everything else is optional. The new run uses Kubernetes
83+
`metadata.generateName`, so the apiserver assigns the random suffix and the
84+
resolved name is read back and printed.
85+
86+
```bash
87+
krci pipelinerun start foo-build
88+
```
89+
90+
```
91+
NAME STATUS PROJECT PR AUTHOR TYPE STARTED DURATION
92+
foo-build-run-zhqvj Pending - - - build 2026-05-07T06:14:04Z -
93+
```
94+
95+
### Flags
96+
97+
| Flag | Description |
98+
|----------------|------------------------------------------------------------------------------|
99+
| `--param` | Pipeline parameter as `key=value` (repeatable; split on first `=`) |
100+
| `--label` | Label to attach to the resulting PipelineRun as `key=value` (repeatable) |
101+
| `--dry-run` | Render the would-be PipelineRun without creating it (needs `-o json`/`yaml`) |
102+
| `-o, --output` | `table` (default), `json`, or `yaml` (yaml only with `--dry-run`) |
103+
104+
> **Params without a default** are submitted with `value: ""` (or `[]` for
105+
> arrays). Pass `--param k=v` for any values your pipeline actually needs.
106+
107+
### Examples
108+
109+
```bash
110+
# Override a single param
111+
krci pipelinerun start foo-build --param git-revision=develop
112+
113+
# Multiple params plus a discoverability label
114+
krci pipelinerun start foo-build --param k=v --param k2=v2 \
115+
--label app.edp.epam.com/codebase=my-app
116+
117+
# Render the would-be PipelineRun without creating it
118+
krci pipelinerun start foo-build --dry-run -o yaml
119+
120+
# JSON output (for AI agents / scripting)
121+
krci pipelinerun start foo-build -o json
122+
```
123+
124+
### JSON output
125+
126+
`start` uses a wrapped envelope (different from `list` / `get`):
127+
128+
```json
129+
{
130+
"schemaVersion": "1",
131+
"data": {
132+
"name": "foo-build-run-zhqvj",
133+
"status": "Pending",
134+
"project": "my-app",
135+
"pr": "",
136+
"author": "",
137+
"type": "build",
138+
"started": "2026-05-07T06:14:04Z",
139+
"duration": ""
140+
}
141+
}
142+
```
143+
144+
For `--dry-run`, `data` is the rendered PipelineRun manifest itself instead
145+
of the result row.
146+
147+
### Finding the run you just started
148+
149+
```bash
150+
krci pipelinerun list --project my-app
151+
```
152+
76153
## Failure diagnosis (`--reason`)
77154

78155
Works on both `list` (targets the most recent matching run) and `get`:
@@ -100,7 +177,9 @@ Logs: sonar
100177
[sonar-scanner] ERROR: QUALITY GATE STATUS: FAILED
101178
```
102179

103-
## JSON output
180+
## JSON output (`list` / `get`)
181+
182+
`start` uses a different envelope — see the [`start` section](#pipelinerun-start) above.
104183

105184
```bash
106185
krci run list --project keycloak-operator -o json

e2e/pipelinerun/fixtures.env.example

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,55 @@
22
# The /e2e command loads this file (if present) before running portal rows.
33
# If a row references a placeholder that has no value, the row is SKIPped.
44
#
5-
# Discover good values with:
6-
# ./dist/krci pipelinerun list -o json | jq '.pipelineRuns[0]'
7-
# ./dist/krci pipelinerun list --status failed -o json | jq '.pipelineRuns[0]'
5+
# All e2e rows are now self-contained: every value below either points at a
6+
# Pipeline defined in fixtures/*.yaml, or at a label we attach to runs we
7+
# create ourselves. There is no dependency on pre-existing cluster workload.
8+
#
9+
# Bootstrap once per cluster:
10+
# kubectl apply -n <namespace> -f source/cli/e2e/pipelinerun/fixtures/
11+
#
12+
# Then run a few starts to populate RUN_NAME / FAILED_RUN_NAME (see below).
13+
14+
# ----- pipelinerun start fixtures -----
15+
# Pipelines defined in fixtures/*.yaml.
16+
PIPELINE_OK=krci-cli-e2e-noop
17+
PIPELINE_REQUIRED_PARAM=krci-cli-e2e-required
18+
PIPELINE_REQUIRED_PARAM_NAME=message
19+
PIPELINE_BROKEN_TT=krci-cli-e2e-broken-tt
20+
# Vanilla Tekton Pipeline with no KRCI labels at all — proves CLI start works
21+
# regardless of the KubeRocketCI labelling convention (PR-S-BARE).
22+
PIPELINE_BARE=krci-cli-e2e-bare
23+
# Pipeline + TriggerTemplate fixture pair — exercises the TT branch of
24+
# createPipelineRunDraftFromPipeline (placeholder resolution, label
25+
# sanitization). The TT name is referenced via the Pipeline's
26+
# app.edp.epam.com/triggertemplate label.
27+
PIPELINE_WITH_TT=krci-cli-e2e-with-tt
28+
TRIGGER_TEMPLATE=krci-cli-e2e-tt
29+
30+
# Param accepted by PIPELINE_OK whose default is "main" — PR-S-2 overrides it.
31+
PARAM_KNOWN_KEY=git-revision
32+
PARAM_KNOWN_VALUE=develop
833

9-
PROJECT=my-project
10-
PR=123
11-
AUTHOR=john-doe
34+
# Synthetic codebase label. Attached at start time via --label so the resulting
35+
# PipelineRun is discoverable via `pipelinerun list --project krci-cli-e2e`.
36+
PIPELINE_LABEL_KEY=app.edp.epam.com/codebase
37+
PIPELINE_LABEL_VALUE=krci-cli-e2e
38+
39+
# ----- pipelinerun list / get fixtures -----
40+
# Identifying values for runs created by the e2e fixtures themselves. The
41+
# pipelinetype filter is the most useful selector since we own the value.
42+
PROJECT=krci-cli-e2e
43+
PR=1
44+
AUTHOR=krci-cli-e2e-bot
1245
BRANCH=main
13-
RUN_NAME=build-my-project-main-abc12
14-
FAILED_RUN_NAME=review-my-project-main-xyz34
46+
47+
# Concrete run names. After a fresh apply, populate these with one successful
48+
# and one failed run name from your namespace, e.g.:
49+
# ./dist/krci pipelinerun start krci-cli-e2e-noop -o json \
50+
# | jq -r '.data.row.name'
51+
# ./dist/krci pipelinerun start krci-cli-e2e-noop \
52+
# --param should-fail=true -o json | jq -r '.data.row.name'
53+
RUN_NAME=
54+
FAILED_RUN_NAME=
55+
1556
NONCE=test-999zzz

e2e/pipelinerun/fixtures/README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# pipelinerun start — e2e fixtures
2+
3+
Self-contained Tekton manifests used by the `krci pipelinerun start` e2e rows
4+
in `../test-cases.md`. Files are the source of truth — apply with
5+
`kubectl apply -f` and the cluster matches what's checked in (no drift).
6+
7+
All pipelines run a single inline `taskSpec` that calls `busybox` and echoes
8+
its inputs. None of them reach external systems, push artifacts, or write to
9+
git. They are safe to start repeatedly. All resource names use a synthetic
10+
`krci-cli-e2e-*` prefix and the manifests carry no organisation-specific
11+
identifiers — drop them into any namespace on any KubeRocketCI cluster.
12+
13+
## Bundle
14+
15+
| File | Resource | Purpose | Test rows |
16+
|---|---|---|---|
17+
| `pipeline-noop.yaml` | Pipeline `krci-cli-e2e-noop` | Four params, all with defaults; `should-fail=true` makes the run exit non-zero. | `PR-S-1`, `PR-S-2`, `PR-S-3`, `PR-S-LABEL`, `PR-S-DRY-YAML`, `PR-S-DRY-JSON`, `PR-S-RACE`, `PR-S-GENNAME`, `PR-S-COL-EQ` |
18+
| `pipeline-required.yaml` | Pipeline `krci-cli-e2e-required` | Declares a no-default param. Documents the portal's empty-string synthesis: omitting `--param message=...` produces `spec.params[0].value: ""`, **not** an admission rejection. The fixture's spec.description still references the old (incorrect) intent — left untouched to avoid drift; see `PR-S-PARAM-SYNTHESIZED` for the actual contract. | `PR-S-PARAM-SYNTHESIZED` |
19+
| `pipeline-broken-tt.yaml` | Pipeline `krci-cli-e2e-broken-tt` | Carries an `app.edp.epam.com/triggertemplate` label pointing at a TT that does not exist. | `PR-S-TT-MISSING` |
20+
| `pipeline-bare.yaml` | Pipeline `krci-cli-e2e-bare` | No KRCI labels at all. Proves the CLI can start any valid Tekton Pipeline regardless of the KubeRocketCI labelling convention. | `PR-S-BARE` |
21+
| `pipeline-with-tt.yaml` | Pipeline `krci-cli-e2e-with-tt` | Paired with `trigger-template.yaml`. Exercises the TriggerTemplate branch of `createPipelineRunDraftFromPipeline` — placeholder resolution and label sanitization. | `PR-S-TT-OK`, `PR-S-TT-DRY` |
22+
| `trigger-template.yaml` | TriggerTemplate `krci-cli-e2e-tt` | Resourcetemplate uses `$(tt.params.X)` placeholders that resolve against `pipeline-with-tt.yaml`'s param defaults. | (paired with above) |
23+
24+
## Apply
25+
26+
Substitute `<namespace>` with whatever namespace you target (e.g. the one your
27+
portal session points at):
28+
29+
```sh
30+
kubectl apply -n <namespace> -f source/cli/e2e/pipelinerun/fixtures/
31+
```
32+
33+
Verify:
34+
35+
```sh
36+
kubectl -n <namespace> get pipeline.tekton.dev,triggertemplate.triggers.tekton.dev \
37+
-l app.edp.epam.com/pipelinetype=tests
38+
```
39+
40+
## Smoke test (after apply)
41+
42+
```sh
43+
# Dry-run — proves CLI <-> portal plumbing without creating a PipelineRun.
44+
./dist/krci pipelinerun start krci-cli-e2e-noop --dry-run -o json | jq .
45+
46+
# Real run with default params.
47+
./dist/krci pipelinerun start krci-cli-e2e-noop -o json
48+
49+
# Real run overriding a param.
50+
./dist/krci pipelinerun start krci-cli-e2e-noop \
51+
--param git-revision=develop --param count=3 -o json
52+
53+
# Deterministic failure path.
54+
./dist/krci pipelinerun start krci-cli-e2e-noop --param should-fail=true -o json
55+
56+
# No-default-param path: succeeds with synthesised empty value (exit 0).
57+
# Demonstrates that "missing required param" is not a reachable failure mode.
58+
./dist/krci pipelinerun start krci-cli-e2e-required -o json
59+
60+
# TriggerTemplate-not-found path: synthesised "TT does not exist" message, exit 1.
61+
./dist/krci pipelinerun start krci-cli-e2e-broken-tt
62+
63+
# Bare Tekton Pipeline (no KRCI labels) — must still start.
64+
./dist/krci pipelinerun start krci-cli-e2e-bare -o json
65+
66+
# TriggerTemplate happy path — dry-run reveals resolved $(tt.params.X) values.
67+
./dist/krci pipelinerun start krci-cli-e2e-with-tt --dry-run -o json | jq .
68+
69+
# TriggerTemplate happy path — real run.
70+
./dist/krci pipelinerun start krci-cli-e2e-with-tt -o json
71+
```
72+
73+
## Cleanup
74+
75+
The PipelineRun objects created by tests are subject to whatever GC the
76+
cluster has configured (Tekton Results retention, operator pruning, etc.).
77+
To remove the fixture resources themselves:
78+
79+
```sh
80+
kubectl delete -n <namespace> -f source/cli/e2e/pipelinerun/fixtures/
81+
```
82+
83+
To purge accumulated runs (label selector picks up runs from any of the
84+
fixture pipelines because every fixture sets `pipelinetype: tests` on either
85+
the Pipeline or — via TT-resolved labels — the resulting PipelineRun):
86+
87+
```sh
88+
kubectl -n <namespace> delete pipelinerun.tekton.dev \
89+
-l app.edp.epam.com/pipelinetype=tests
90+
```
91+
92+
## Why these manifests look the way they do
93+
94+
- **Labelled `pipelinetype: tests`.** Distinguishes these fixtures from
95+
KubeRocketCI's real pipelines (build/review/deploy/clean/security/release).
96+
Filter via `kubectl get pipeline.tekton.dev
97+
-l app.edp.epam.com/pipelinetype=tests` or the same selector in the portal
98+
UI. `tests` is already a valid value in the portal's `pipelineTypeEnum`.
99+
- **`app.edp.epam.com/*` label keys are upstream KubeRocketCI definitions**
100+
and cannot be renamed — the portal reads exactly those keys to drive
101+
TriggerTemplate lookup, codebase filters, and pipelinetype filters. The
102+
values used in this bundle (`tests`, `krci-cli-e2e`, `""`) are synthetic
103+
and carry no organisation-specific data.
104+
- **`triggertemplate` label is empty** on the `noop` and `required` fixtures.
105+
The portal's Pipeline Zod schema requires both labels to be present, but
106+
`getTriggerTemplateLabel` treats an empty string as "absent" and skips the
107+
TT lookup — so `start` works without us having to also create a TT.
108+
`pipeline-broken-tt.yaml` is the deliberate exception: its label points at
109+
a TT that does not exist, which is exactly the failure mode it tests.
110+
- **`pipeline-with-tt.yaml` + `trigger-template.yaml` are a pair.** The
111+
Pipeline carries `triggertemplate: krci-cli-e2e-tt`; the matching TT's
112+
`resourcetemplates[0]` uses `$(tt.params.X)` placeholders. The portal's
113+
draft builder resolves those placeholders against the Pipeline's param
114+
defaults (not the TT's), so the resulting PipelineRun reflects values
115+
declared on the Pipeline side. This is the only fixture that exercises
116+
`createPipelineRunDraftFromPipeline`'s TT branch end-to-end.
117+
- **Inline `taskSpec`, not `taskRef`.** Decouples the fixtures from
118+
cluster-installed Tasks/ClusterTasks — apply works on any cluster with
119+
Tekton Pipelines installed.
120+
- **Params passed via env, not interpolated into shell.** Tekton substitutes
121+
`$(params.X)` as text before the shell runs. Routing through `env:` means
122+
param values land in shell variables, where `"$VAR"` does not re-expand
123+
command substitutions — so a hostile `--param message='$(reboot)'` is
124+
echoed literally rather than executed.
125+
- **Pinned image tag (`busybox:1.36`).** Reproducible across runs.

0 commit comments

Comments
 (0)