From 286b1e18307abfce0f2d66ffba6fd012255742aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 16:43:08 +0000 Subject: [PATCH] feat(m16): retire PipelineRun escape hatches Flask always creates StackRuns; STACKRUN_VIA_CRD=false is ignored. generate-run.sh --pipeline-run is removed. Helm no longer opts out. Co-authored-by: John Menke --- DO-THIS-LOCAL.md | 5 +- README.md | 2 +- docs/SCRIPTS.md | 2 +- docs/demos/docgen.yaml | 15 +- docs/demos/hints/project-context.md | 2 +- docs/demos/hints/segment-08-orchestrator.md | 5 +- .../demos/hints/segment-18-roadmap-forward.md | 3 +- .../hints/segment-19-kubernetes-operator.md | 9 +- helm/tekton-dag/README.md | 3 +- .../templates/orchestration-deployment.yaml | 4 +- helm/tekton-dag/values.yaml | 3 - milestones/milestone-16.md | 6 +- orchestrator/app.py | 7 +- orchestrator/routes.py | 148 ++--------- orchestrator/tests/conftest.py | 1 - orchestrator/tests/test_routes.py | 236 ++++++++---------- orchestrator/tests/test_stackrun_crd_path.py | 23 +- scripts/generate-run.sh | 196 ++------------- scripts/run-cluster-ci.sh | 3 - scripts/verify-m4-stacks-and-labels.sh | 6 + 20 files changed, 198 insertions(+), 481 deletions(-) diff --git a/DO-THIS-LOCAL.md b/DO-THIS-LOCAL.md index bc7a72c..a27bb75 100644 --- a/DO-THIS-LOCAL.md +++ b/DO-THIS-LOCAL.md @@ -154,11 +154,10 @@ CRD-primary path (see [milestones/milestone-14.md](milestones/milestone-14.md)): # Newman against orchestrator creating StackRuns (skips isolation/phase2 if already green) ./scripts/run-cluster-ci.sh --skip-isolation --skip-phase2 --with-operator -# Or Helm (also sets STACKRUN_VIA_CRD when operator.enabled) +# Or Helm (Flask always creates StackRuns; operator.enabled still controls the manager) cd helm/tekton-dag && ./package.sh helm upgrade --install tekton-dag . -n tekton-pipelines \ - --set operator.enabled=true \ - --set orchestrationService.stackrunViaCrd=true + --set operator.enabled=true # Expect Stack status.valid=true and StackRun to create a PipelineRun kubectl get stacks,stackruns -n tekton-pipelines diff --git a/README.md b/README.md index 5bc1a58..20732f3 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Each row links to the **in-browser player** on Pages (`#seg-…`) and to the **c | [M13](milestones/milestone-13.md) | **Partial** | Production hardening foundations shipped: webhook HMAC, stack secrets/config + deploy wiring + injection-status APIs, PipelineRun timeouts / task retries / failure classifier / resource profiles, `stack-promote` + registries + approval gate. Open: intercept secret/config wiring, Helm `appConfig` / ESO, GUI panels, observability, cross-cluster deploy. Roadmap video: [segment 18](https://jmjava.github.io/tekton-dag/#seg-18). Local cluster checklist: [DO-THIS-LOCAL.md](DO-THIS-LOCAL.md). | | [M14](milestones/milestone-14.md) | **Partial** | **Kubernetes operator (CRD-primary, default-on):** `Stack` + `StackRun` + `Team` (`tektondag.io/v1alpha1`). Helm `operator.enabled` defaults **true**; Flask/GUI/Triggers/`generate-run.sh` create StackRuns. Kind soak path: `scripts/install-operator-kind.sh`, `run-cluster-ci.sh` (operator on unless `--skip-operator`). Hygiene: [M15](milestones/milestone-15.md). Follow-ons: [M16](milestones/milestone-16.md). | | [M15](milestones/milestone-15.md) | **Completed** | Control-plane hygiene: idempotent StackRun→PipelineRun, GHA `--skip-operator`, Triggers `prNumber`, Flask/GUI promote approval, soak `ready==total`, dead PipelineRun builders. Squash-merged [#19](https://github.com/jmjava/tekton-dag/pull/19). | -| [M16](milestones/milestone-16.md) | **In progress** | Team CR overlay for Flask/GUI, `spec.continueFrom` → `stack-pr-continue`, Kind Stack admission webhook installer. Remaining: retire PipelineRun escape hatches, demo narration/MP4 rebuild, S34. | +| [M16](milestones/milestone-16.md) | **In progress** | Team CR overlay for Flask/GUI, `spec.continueFrom` → `stack-pr-continue`, Kind Stack admission webhook installer, PipelineRun escape hatches retired. Remaining: demo narration/MP4 rebuild, S34. | Older milestones (M2, M3) are in [milestones/completed/](milestones/completed/). diff --git a/docs/SCRIPTS.md b/docs/SCRIPTS.md index 9363b7f..296561a 100644 --- a/docs/SCRIPTS.md +++ b/docs/SCRIPTS.md @@ -55,7 +55,7 @@ Shared helpers live in [`scripts/common.sh`](../scripts/common.sh) (sourced by m |--------|---------| | [`publish-build-images.sh`](../scripts/publish-build-images.sh) | Build/push **compile** images (polyglot builders). | | [`publish-orchestrator-image.sh`](../scripts/publish-orchestrator-image.sh) | Build/push **orchestrator** image. | -| [`generate-run.sh`](../scripts/generate-run.sh) | Emit/apply a **StackRun** (operator). Pass `--pipeline-run` for a raw Tekton PipelineRun (`pr` / `merge` only). | +| [`generate-run.sh`](../scripts/generate-run.sh) | Emit/apply a **StackRun** (operator). `--pipeline-run` was removed in M16. | | [`promote-pipelines.sh`](../scripts/promote-pipelines.sh) | Promote pipeline definitions across environments/namespaces. | | [`create-and-push-sample-repos.sh`](../scripts/create-and-push-sample-repos.sh) | Sample app repos for demos/regression. | | [`ensure-git-ssh-secret.sh`](../scripts/ensure-git-ssh-secret.sh) | Git SSH secret for cluster git operations. | diff --git a/docs/demos/docgen.yaml b/docs/demos/docgen.yaml index ce075ab..9b8f88a 100644 --- a/docs/demos/docgen.yaml +++ b/docs/demos/docgen.yaml @@ -274,7 +274,7 @@ narration_from_source: operator default-on (Stack/StackRun/Team CRs).' - Do not frame milestone 13 as entirely future work. Say foundations are shipped and name only remaining open items when discussing production hardening. - - The Kubernetes operator under operator/ is CRD-primary (Stack + StackRun + Team, tektondag.io/v1alpha1). Helm operator.enabled defaults on. Flask STACKRUN_VIA_CRD is true when the operator is installed. + - The Kubernetes operator under operator/ is CRD-primary (Stack + StackRun + Team, tektondag.io/v1alpha1). Helm operator.enabled defaults on. Flask always creates StackRun CRs. - Demo video for this bundle is Manim + TTS + ffmpeg compose only (current docgen). Do not narrate VHS, ttyd, or terminal-tape capture as a pipeline stage. - 'Timestamps for this bundle must use Whisper (timestamps.engine: whisper). Do not use the local silencedetect aligner @@ -368,9 +368,8 @@ narration_from_source: 08: hints: - 'Flask orchestrator on 8080: healthz/readyz, stacks API, POST /api/run modes including promote.' - - Orchestrator still owns webhook HMAC (fail-closed when named Secret missing) and stack resolve. When STACKRUN_VIA_CRD - is true it creates StackRun CRs instead of PipelineRuns — operator reconciles (see segment 19). Default remains direct - PipelineRun creation. + - Orchestrator still owns webhook HMAC (fail-closed when named Secret missing) and stack resolve. Flask always + creates StackRun CRs — operator reconciles (see segment 19). context: paths: - README.md @@ -461,8 +460,8 @@ narration_from_source: timeouts/retries/classifier/profiles, promote+registries).' - 'Remaining M13 open items only: intercept secret wiring, Helm appConfig/ESO templates, GUI panels, Prometheus/cost labels, cross-cluster deploy, Results/Neo4j backup polish.' - - 'Post-M14 follow-ons: GUI native StackRun views / promote approve via StackRun.spec.approvedBy, enable Stack - admission webhook with certs, retire --pipeline-run / STACKRUN_VIA_CRD=false escape hatches.' + - 'Post-M14 follow-ons: M15 hygiene and M16 Team overlay / continueFrom / webhook installer / escape-hatch + retirement are landed. Remaining: spoken demo rebuild; S34 intercept E2E is parked.' context: paths: - milestones/milestone-13.md @@ -475,8 +474,8 @@ narration_from_source: - 'CRDs: Stack (validate/topo status) vs StackRun (mode pr|bootstrap|merge|promote -> PipelineRun).' - 'Flow: GitHub/API -> Flask (HMAC+resolve) -> StackRun -> tekton-dag-operator -> PipelineRun (orphaned on delete for Results history).' - - 'Enablement: Helm operator.enabled and Kind STACKRUN_VIA_CRD default on. Golden Python↔Go PipelineRun builders.' - - 'Status: CRD-primary is the default runtime; webhook certs and GUI StackRun views remain follow-ons.' + - 'Enablement: Helm operator.enabled default on. Flask always creates StackRuns. Golden Python↔Go PipelineRun builders.' + - 'Status: CRD-primary is the default runtime; M15/M16 control-plane follow-ons except spoken demo rebuild are landed.' context: paths: - operator/README.md diff --git a/docs/demos/hints/project-context.md b/docs/demos/hints/project-context.md index 31b4289..957d9d7 100644 --- a/docs/demos/hints/project-context.md +++ b/docs/demos/hints/project-context.md @@ -19,7 +19,7 @@ docgen: only remaining open items when discussing production hardening. - >- The Kubernetes operator under operator/ is CRD-primary (Stack + StackRun + Team, - tektondag.io/v1alpha1). Flask STACKRUN_VIA_CRD and Helm operator.enabled default on. + tektondag.io/v1alpha1). Flask always creates StackRun CRs. Helm operator.enabled defaults on. - >- Demo video for this bundle is Manim + TTS + ffmpeg compose only (current docgen). Do not narrate VHS, ttyd, or terminal-tape capture as a pipeline stage. diff --git a/docs/demos/hints/segment-08-orchestrator.md b/docs/demos/hints/segment-08-orchestrator.md index 4e81e5c..6b000f8 100644 --- a/docs/demos/hints/segment-08-orchestrator.md +++ b/docs/demos/hints/segment-08-orchestrator.md @@ -16,9 +16,8 @@ docgen: including promote. - >- Orchestrator still owns webhook HMAC (fail-closed when named Secret missing) and - stack resolve. When STACKRUN_VIA_CRD is true it creates StackRun CRs instead of - PipelineRuns — operator reconciles (see segment 19). Default remains direct - PipelineRun creation. + stack resolve. Flask always creates StackRun CRs; the operator reconciles + them to PipelineRuns (see segment 19). context: paths: - README.md diff --git a/docs/demos/hints/segment-18-roadmap-forward.md b/docs/demos/hints/segment-18-roadmap-forward.md index acceab0..ee127aa 100644 --- a/docs/demos/hints/segment-18-roadmap-forward.md +++ b/docs/demos/hints/segment-18-roadmap-forward.md @@ -21,8 +21,7 @@ docgen: Results/Neo4j backup polish. - >- Post-M14: M15 control-plane hygiene (idempotent PipelineRun create). - Remaining M16: Team CR as Flask/GUI source, Stack admission webhook with - certs, retire --pipeline-run / STACKRUN_VIA_CRD=false escape hatches. + Remaining M16: demo spoken narration/MP4 rebuild. S34 intercept E2E is parked. context: paths: - milestones/milestone-13.md diff --git a/docs/demos/hints/segment-19-kubernetes-operator.md b/docs/demos/hints/segment-19-kubernetes-operator.md index 2f52c3e..cf7c8e4 100644 --- a/docs/demos/hints/segment-19-kubernetes-operator.md +++ b/docs/demos/hints/segment-19-kubernetes-operator.md @@ -21,11 +21,12 @@ docgen: Flow: GitHub/API -> Flask (HMAC+resolve) -> StackRun -> tekton-dag-operator -> PipelineRun (orphaned on delete for Results history). - >- - Enablement: Helm operator.enabled and Kind STACKRUN_VIA_CRD default on. Golden - Python↔Go PipelineRun builders. + Enablement: Helm operator.enabled default on. Flask always creates StackRuns. + Golden Python↔Go PipelineRun builders. - >- - Status: CRD-primary is the default runtime; M15 hygiene (idempotent - create) in progress. Webhook certs and Team CR as API source remain M16. + Status: CRD-primary is the default runtime; M15 hygiene and M16 Team overlay / + continueFrom / webhook installer / escape-hatch retirement are landed. Spoken + demo rebuild remains M16. context: paths: - operator/README.md diff --git a/helm/tekton-dag/README.md b/helm/tekton-dag/README.md index a0f894a..3e436ff 100644 --- a/helm/tekton-dag/README.md +++ b/helm/tekton-dag/README.md @@ -79,7 +79,6 @@ Packaged content under `raw/` is **not** committed by default; `package.sh` copi | `orchestrationService.replicas` | int | `1` | Replica count | | `orchestrationService.port` | int | `8080` | Container and Service port | | `orchestrationService.resources` | object | requests/limits | Pod resources | -| `orchestrationService.stackrunViaCrd` | bool | `true` | When `operator.enabled`, set `STACKRUN_VIA_CRD` (set `false` to keep direct PipelineRun creates) | | `operator.enabled` | bool | `true` | Deploy M14 Go operator + use CRDs from chart `crds/` | | `operator.image` | string | `localhost:5000/tekton-dag-operator:latest` | Operator manager image | | `operator.imagePullPolicy` | string | `Always` | Pull policy (`Always` for Kind local registry) | @@ -90,6 +89,8 @@ Packaged content under `raw/` is **not** committed by default; `package.sh` copi | `triggers.githubTokenSecretName` | string | `"github-token"` | Secret name for PR comment token (pipelines) | | `dashboard.url` | string | `""` | Optional Tekton Dashboard base URL for PR links | +The orchestrator always creates **StackRun** CRs (`STACKRUN_VIA_CRD` is hard-coded `"true"`). `orchestrationService.stackrunViaCrd` was removed in M16; `--pipeline-run` on `generate-run.sh` is unsupported. + `compileImages` / `compileImageVariants` are the **source of truth** for image URLs you pass into PipelineRuns as `compile-image-*` parameters (see `scripts/generate-run.sh`). Wiring those params from Helm into every trigger is cluster-specific; keep values aligned with the images you push. ## Multi-team setup diff --git a/helm/tekton-dag/templates/orchestration-deployment.yaml b/helm/tekton-dag/templates/orchestration-deployment.yaml index c7c688d..483c273 100644 --- a/helm/tekton-dag/templates/orchestration-deployment.yaml +++ b/helm/tekton-dag/templates/orchestration-deployment.yaml @@ -55,8 +55,8 @@ spec: - name: REGISTRIES_FILE value: {{ .Values.orchestrationService.registriesFile | quote }} - name: STACKRUN_VIA_CRD - # On when operator is enabled (unless stackrunViaCrd is explicitly false). - value: {{ if and .Values.operator.enabled (ne .Values.orchestrationService.stackrunViaCrd false) }}"true"{{ else }}"false"{{ end }} + # Retired M16: Flask always creates StackRuns. Kept true so mixed-image rollouts stay on the CRD path. + value: "true" - name: STACKS_DIR value: /stacks - name: TEAMS_DIR diff --git a/helm/tekton-dag/values.yaml b/helm/tekton-dag/values.yaml index 06838fb..3ba0fae 100644 --- a/helm/tekton-dag/values.yaml +++ b/helm/tekton-dag/values.yaml @@ -67,9 +67,6 @@ orchestrationService: # Verify GitHub webhooks when the named Secret exists (key: secret|value|webhook-secret) webhookVerifySignature: true registriesFile: "/stacks/registries.yaml" - # M14: when operator.enabled, defaults to creating StackRun CRs. - # Set explicitly to false to keep direct PipelineRun creation while operator is installed. - stackrunViaCrd: true resources: requests: cpu: "100m" diff --git a/milestones/milestone-16.md b/milestones/milestone-16.md index 7c047f4..7fb3f58 100644 --- a/milestones/milestone-16.md +++ b/milestones/milestone-16.md @@ -1,6 +1,6 @@ # Milestone 16 — Control-plane follow-ons -**Status:** In progress (M15 landed as #19). +**Status:** In progress (M15 landed as #19; Team overlay / continueFrom / webhook installer as #20; escape hatches retired on this follow-on). **Goal:** Finish the remaining dual sources of truth after M15 hygiene. Still **no new CRD kinds**. @@ -8,7 +8,7 @@ - [x] Flask `StackResolver` and GUI `TeamRegistry` overlay **Team CRs** when a loader is wired; Git `teams/*/team.yaml` remains the GitOps source that `apply-stack-crs.sh` / `package.sh` mirror - [x] Kind webhook installer (`scripts/install-operator-webhook-kind.sh`): certs + Service + ValidatingWebhookConfiguration; `ENABLE_WEBHOOKS=true` only with those certs. Helm still does not apply Fail-closed admission by default. -- [ ] Retire `--pipeline-run` / Flask `STACKRUN_VIA_CRD=false` once every documented cluster path runs the operator +- [x] Retire `--pipeline-run` / Flask `STACKRUN_VIA_CRD=false` (Flask always creates StackRuns; Helm env stays `"true"` for mixed-image rollouts; `--skip-operator` no longer flips the flag) - [x] `spec.continueFrom` on StackRun + operator `stack-pr-continue` builder; `rerun-pr-from.sh` creates a StackRun - [ ] Demo **hints** already describe default-on; rebuild **spoken** `docs/demos/narration/*.md` + MP4s via `cd docs/demos && docgen rebuild-after-audio` (do not `compose` alone) - [ ] S34 intercept E2E remains a separate stop — do not start unless explicitly requested @@ -17,7 +17,7 @@ 1. GUI/Flask team list matches Team CRs applied from Git YAML (overlay). 2. Webhook installer exists; Kind without the installer still does not apply Fail-closed admission. -3. Escape hatches removed or documented as unsupported. +3. Escape hatches removed: `--pipeline-run` and `STACKRUN_VIA_CRD=false` are unsupported. 4. Narration lint + `docgen validate --pre-push` green after the audio rebuild. ## Do not diff --git a/orchestrator/app.py b/orchestrator/app.py index 2bf05af..a0e4412 100644 --- a/orchestrator/app.py +++ b/orchestrator/app.py @@ -1,7 +1,8 @@ """ tekton-dag orchestration service. -Receives GitHub webhooks, resolves stacks, creates Tekton PipelineRuns. +Receives GitHub webhooks, resolves stacks, creates StackRun CRs. +The operator reconciles those into Tekton PipelineRuns. Runs as an in-cluster pod alongside Tekton. """ @@ -46,9 +47,7 @@ def create_app(): "REGISTRIES_FILE", os.path.join(os.environ.get("STACKS_DIR", "/stacks"), "registries.yaml"), ), - # M14: when true, create StackRun CRs instead of PipelineRuns directly. - STACKRUN_VIA_CRD=os.environ.get("STACKRUN_VIA_CRD", "false").lower() - in ("1", "true", "yes"), + # Retired M16: Flask always creates StackRun CRs. STACKRUN_VIA_CRD env is ignored. ) stacks_dir = os.environ.get("STACKS_DIR", "/stacks") diff --git a/orchestrator/routes.py b/orchestrator/routes.py index a37c57f..d893211 100644 --- a/orchestrator/routes.py +++ b/orchestrator/routes.py @@ -3,10 +3,10 @@ Endpoints: POST /webhook/github - GitHub webhook handler (PR opened/merged) - POST /api/run - Manual PipelineRun trigger + POST /api/run - Manual StackRun trigger POST /api/bootstrap - Trigger bootstrap pipeline GET /api/stacks - List registered stacks - GET /api/runs - List recent PipelineRuns + GET /api/runs - List recent StackRuns GET /api/apps//injection-status - Secrets/config injection status GET /healthz - Liveness probe GET /readyz - Readiness probe @@ -18,7 +18,6 @@ from flask import Flask, request, jsonify, current_app import k8s_client -import pipelinerun_builder as builder import stackrun_builder import graph_client import webhook_auth @@ -47,24 +46,16 @@ def _reliability_kwargs(cfg, data=None): return {"timeout": timeout, "max_retries": max_retries} -def _via_crd(cfg) -> bool: - return bool(cfg.get("STACKRUN_VIA_CRD")) - - -def _create_run(cfg, *, mode: str, pipelinerun_manifest=None, stackrun_kwargs=None): - """ - Create a PipelineRun or StackRun depending on STACKRUN_VIA_CRD. +def _create_run(cfg, *, mode: str, stackrun_kwargs=None): + """Create a StackRun CR. Newman keeps a pipelinerun alias (same name until reconcile). Returns (response_dict, status_code). """ ns = cfg["NAMESPACE"] try: - if _via_crd(cfg): - manifest = stackrun_builder.build_stackrun(mode=mode, namespace=ns, **(stackrun_kwargs or {})) - name = k8s_client.create_stackrun(manifest, namespace=ns) - return {"status": "created", "stackrun": name, "pipelinerun": name, "mode": mode}, 200 - name = k8s_client.create_pipelinerun(pipelinerun_manifest, namespace=ns) - return {"status": "created", "pipelinerun": name, "mode": mode}, 200 + manifest = stackrun_builder.build_stackrun(mode=mode, namespace=ns, **(stackrun_kwargs or {})) + name = k8s_client.create_stackrun(manifest, namespace=ns) + return {"status": "created", "stackrun": name, "pipelinerun": name, "mode": mode}, 200 except Exception as e: return {"error": str(e)}, 500 @@ -130,26 +121,14 @@ def list_runs(): ns = cfg["NAMESPACE"] limit = request.args.get("limit", 20, type=int) summary = [] - if _via_crd(cfg): - runs = k8s_client.list_stackruns(namespace=ns, limit=limit) - for r in runs: - status = r.get("status", {}) - summary.append({ - "name": r["metadata"]["name"], - "pipeline": r.get("spec", {}).get("mode", ""), - "status": status.get("phase", "Unknown"), - "pipelinerun": status.get("pipelineRunName", ""), - "created": r["metadata"].get("creationTimestamp", ""), - }) - return jsonify(summary) - runs = k8s_client.list_pipelineruns(namespace=ns, limit=limit) + runs = k8s_client.list_stackruns(namespace=ns, limit=limit) for r in runs: - conditions = r.get("status", {}).get("conditions", [{}]) - reason = conditions[0].get("reason", "Unknown") if conditions else "Unknown" + status = r.get("status", {}) summary.append({ "name": r["metadata"]["name"], - "pipeline": r["metadata"].get("labels", {}).get("tekton.dev/pipeline", ""), - "status": reason, + "pipeline": r.get("spec", {}).get("mode", ""), + "status": status.get("phase", "Unknown"), + "pipelinerun": status.get("pipelineRunName", ""), "created": r["metadata"].get("creationTimestamp", ""), }) return jsonify(summary) @@ -184,15 +163,6 @@ def manual_run(): intercept_backend = data.get("intercept_backend", cfg["INTERCEPT_BACKEND"]) if mode == "bootstrap": - run = builder.build_bootstrap_pipelinerun( - git_url=git_url, - git_revision=git_revision, - stack_file=stack_file, - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - namespace=cfg["NAMESPACE"], - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=git_url, @@ -205,16 +175,6 @@ def manual_run(): changed_app = data.get("changed_app", "") if not changed_app: return jsonify({"error": "changed_app required for merge"}), 400 - run = builder.build_merge_pipelinerun( - changed_app=changed_app, - git_url=git_url, - git_revision=git_revision, - stack_file=stack_file, - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - namespace=cfg["NAMESPACE"], - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=git_url, @@ -238,12 +198,7 @@ def manual_run(): }), 400 require_approval = bool(data.get("require_approval", False)) approved_by = data.get("approved_by", "") - # Direct PipelineRun create has no PendingApproval phase. CRD path - # matches the GUI: empty approved_by waits on the operator. - if require_approval and not approved_by and not _via_crd(cfg): - return jsonify({ - "error": "approved_by required when require_approval is true", - }), 400 + # Empty approved_by with require_approval waits on operator PendingApproval. registries = registry_resolver.load_registries(cfg.get("REGISTRIES_FILE", "")) target = registry_resolver.resolve_promote_target( target_environment=target_environment, @@ -251,19 +206,6 @@ def manual_run(): credentials_secret=data.get("credentials_secret", ""), registries=registries, ) - run = builder.build_promote_pipelinerun( - stack_file=stack_file, - release_version=release_version, - target_environment=target["target_environment"], - image_registry=cfg["IMAGE_REGISTRY"], - target_registry=target["target_registry"], - credentials_secret=target["credentials_secret"], - changed_app=changed_app, - namespace=cfg["NAMESPACE"], - require_approval=require_approval, - approved_by=approved_by, - **rel, - ) sr_kw = dict( stack_file=stack_file, image_registry=cfg["IMAGE_REGISTRY"], @@ -282,19 +224,6 @@ def manual_run(): if not changed_app or not pr_number: return jsonify({"error": "changed_app and pr_number required for pr"}), 400 app_revisions = data.get("app_revisions", "{}") - run = builder.build_pr_pipelinerun( - stack_file=stack_file, - changed_app=changed_app, - pr_number=pr_number, - git_url=git_url, - git_revision=git_revision, - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - intercept_backend=intercept_backend, - app_revisions=app_revisions, - namespace=cfg["NAMESPACE"], - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=git_url, @@ -308,9 +237,7 @@ def manual_run(): **rel, ) - body, code = _create_run( - cfg, mode=mode, pipelinerun_manifest=run, stackrun_kwargs=sr_kw - ) + body, code = _create_run(cfg, mode=mode, stackrun_kwargs=sr_kw) return jsonify(body), code @app.route("/api/bootstrap", methods=["POST"]) @@ -321,15 +248,6 @@ def bootstrap(): stack_file = data.get("stack_file", cfg["STACK_FILE"]) rel = _reliability_kwargs(cfg, data) - run = builder.build_bootstrap_pipelinerun( - git_url=cfg["GIT_URL"], - git_revision=cfg["GIT_REVISION"], - stack_file=stack_file, - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - namespace=cfg["NAMESPACE"], - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=cfg["GIT_URL"], @@ -338,16 +256,14 @@ def bootstrap(): cache_repo=cfg["CACHE_REPO"], **rel, ) - body, code = _create_run( - cfg, mode="bootstrap", pipelinerun_manifest=run, stackrun_kwargs=sr_kw - ) + body, code = _create_run(cfg, mode="bootstrap", stackrun_kwargs=sr_kw) return jsonify(body), code @app.route("/webhook/github", methods=["POST"]) def github_webhook(): """ GitHub webhook handler. - Validates signature, parses PR event, resolves stack, creates PipelineRun. + Validates signature, parses PR event, resolves stack, creates a StackRun. """ rejected = _verify_webhook_or_reject() if rejected is not None: @@ -385,20 +301,6 @@ def github_webhook(): if action in ("opened", "synchronize", "reopened"): app_rev_json = json.dumps({changed_app: head_sha}) pr_repo_url = pr.get("base", {}).get("repo", {}).get("ssh_url", "") - run = builder.build_pr_pipelinerun( - stack_file=stack_file, - changed_app=changed_app, - pr_number=pr_number, - git_url=cfg["GIT_URL"], - git_revision=cfg["GIT_REVISION"], - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - intercept_backend=cfg["INTERCEPT_BACKEND"], - app_revisions=app_rev_json, - namespace=cfg["NAMESPACE"], - pr_repo_url=pr_repo_url, - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=cfg["GIT_URL"], @@ -412,22 +314,10 @@ def github_webhook(): pr_repo_url=pr_repo_url, **rel, ) - body, code = _create_run( - cfg, mode="pr", pipelinerun_manifest=run, stackrun_kwargs=sr_kw - ) + body, code = _create_run(cfg, mode="pr", stackrun_kwargs=sr_kw) return jsonify(body), code elif action == "closed" and merged: - run = builder.build_merge_pipelinerun( - changed_app=changed_app, - git_url=cfg["GIT_URL"], - git_revision="main", - stack_file=stack_file, - image_registry=cfg["IMAGE_REGISTRY"], - cache_repo=cfg["CACHE_REPO"], - namespace=cfg["NAMESPACE"], - **rel, - ) sr_kw = dict( stack_file=stack_file, git_url=cfg["GIT_URL"], @@ -437,9 +327,7 @@ def github_webhook(): changed_app=changed_app, **rel, ) - body, code = _create_run( - cfg, mode="merge", pipelinerun_manifest=run, stackrun_kwargs=sr_kw - ) + body, code = _create_run(cfg, mode="merge", stackrun_kwargs=sr_kw) return jsonify(body), code return jsonify({"status": "ignored", "reason": f"action={action}"}), 200 diff --git a/orchestrator/tests/conftest.py b/orchestrator/tests/conftest.py index 1311076..aab68e8 100644 --- a/orchestrator/tests/conftest.py +++ b/orchestrator/tests/conftest.py @@ -44,7 +44,6 @@ def flask_app(): PIPELINE_TIMEOUT="2h", MAX_RETRIES=2, REGISTRIES_FILE="", - STACKRUN_VIA_CRD=False, ) resolver = MagicMock(name="StackResolver") resolver.list_stacks.return_value = [ diff --git a/orchestrator/tests/test_routes.py b/orchestrator/tests/test_routes.py index 36fe79b..079e7fe 100644 --- a/orchestrator/tests/test_routes.py +++ b/orchestrator/tests/test_routes.py @@ -37,23 +37,23 @@ def test_api_teams(client, flask_app): assert rv.get_json() == teams -@patch("routes.k8s_client.list_pipelineruns") -def test_api_runs_summarizes_pipelineruns(mock_list, client, flask_app): +@patch("routes.k8s_client.list_stackruns") +def test_api_runs_summarizes_stackruns(mock_list, client, flask_app): mock_list.return_value = [ { "metadata": { - "name": "pr-1", + "name": "stackrun-pr-1", "creationTimestamp": "2024-01-01T00:00:00Z", - "labels": {"tekton.dev/pipeline": "stack-pr-test"}, }, - "status": {"conditions": [{"reason": "Succeeded"}]}, + "spec": {"mode": "pr"}, + "status": {"phase": "Succeeded", "pipelineRunName": "stackrun-pr-1"}, }, { "metadata": { - "name": "pr-2", + "name": "stackrun-pr-2", "creationTimestamp": "", - "labels": {}, }, + "spec": {}, "status": {}, }, ] @@ -61,16 +61,19 @@ def test_api_runs_summarizes_pipelineruns(mock_list, client, flask_app): assert rv.status_code == 200 mock_list.assert_called_once_with(namespace=flask_app.config["NAMESPACE"], limit=10) data = rv.get_json() - assert data[0]["name"] == "pr-1" - assert data[0]["pipeline"] == "stack-pr-test" + assert data[0]["name"] == "stackrun-pr-1" + assert data[0]["pipeline"] == "pr" assert data[0]["status"] == "Succeeded" + assert data[0]["pipelinerun"] == "stackrun-pr-1" assert data[1]["status"] == "Unknown" -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_api_run_pr_success(mock_build_pr, mock_create, client): - mock_build_pr.return_value = {"metadata": {"name": "built"}} +def _created_spec(mock_create): + return mock_create.call_args.args[0]["spec"] + + +@patch("routes.k8s_client.create_stackrun") +def test_api_run_pr_success(mock_create, client): mock_create.return_value = "run-created" rv = client.post( "/api/run", @@ -85,13 +88,22 @@ def test_api_run_pr_success(mock_build_pr, mock_create, client): content_type="application/json", ) assert rv.status_code == 200 - assert rv.get_json() == {"status": "created", "pipelinerun": "run-created", "mode": "pr"} - mock_build_pr.assert_called_once() + assert rv.get_json() == { + "status": "created", + "stackrun": "run-created", + "pipelinerun": "run-created", + "mode": "pr", + } mock_create.assert_called_once() + spec = _created_spec(mock_create) + assert spec["mode"] == "pr" + assert spec["changedApp"] == "fe" + assert spec["prNumber"] == 42 + assert spec["gitRevision"] == "topic" -@patch("routes.builder.build_pr_pipelinerun") -def test_api_run_pr_missing_changed_app(mock_build_pr, client): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_pr_missing_changed_app(mock_create, client): rv = client.post( "/api/run", data=json.dumps({"mode": "pr", "pr_number": 1}), @@ -99,24 +111,22 @@ def test_api_run_pr_missing_changed_app(mock_build_pr, client): ) assert rv.status_code == 400 assert "changed_app" in rv.get_json()["error"] - mock_build_pr.assert_not_called() + mock_create.assert_not_called() -@patch("routes.builder.build_pr_pipelinerun") -def test_api_run_pr_missing_pr_number(mock_build_pr, client): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_pr_missing_pr_number(mock_create, client): rv = client.post( "/api/run", data=json.dumps({"mode": "pr", "changed_app": "fe"}), content_type="application/json", ) assert rv.status_code == 400 - mock_build_pr.assert_not_called() + mock_create.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_merge_pipelinerun") -def test_api_run_merge_success(mock_build_merge, mock_create, client): - mock_build_merge.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_merge_success(mock_create, client): mock_create.return_value = "merge-run" rv = client.post( "/api/run", @@ -125,24 +135,23 @@ def test_api_run_merge_success(mock_build_merge, mock_create, client): ) assert rv.status_code == 200 assert rv.get_json()["mode"] == "merge" - mock_build_merge.assert_called_once() + assert rv.get_json()["stackrun"] == "merge-run" + assert _created_spec(mock_create)["changedApp"] == "api" -@patch("routes.builder.build_merge_pipelinerun") -def test_api_run_merge_missing_changed_app(mock_build_merge, client): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_merge_missing_changed_app(mock_create, client): rv = client.post( "/api/run", data=json.dumps({"mode": "merge"}), content_type="application/json", ) assert rv.status_code == 400 - mock_build_merge.assert_not_called() + mock_create.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_bootstrap_pipelinerun") -def test_api_run_bootstrap_mode(mock_build_boot, mock_create, client): - mock_build_boot.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_bootstrap_mode(mock_create, client): mock_create.return_value = "boot-1" rv = client.post( "/api/run", @@ -151,25 +160,22 @@ def test_api_run_bootstrap_mode(mock_build_boot, mock_create, client): ) assert rv.status_code == 200 assert rv.get_json()["mode"] == "bootstrap" - mock_build_boot.assert_called_once() + assert _created_spec(mock_create)["mode"] == "bootstrap" -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_bootstrap_pipelinerun") -def test_api_bootstrap_route(mock_build_boot, mock_create, client): - mock_build_boot.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_bootstrap_route(mock_create, client): mock_create.return_value = "boot-2" rv = client.post("/api/bootstrap", data=json.dumps({}), content_type="application/json") assert rv.status_code == 200 body = rv.get_json() assert body["status"] == "created" assert body["mode"] == "bootstrap" + assert body["stackrun"] == "boot-2" -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_api_run_k8s_failure(mock_build, mock_create, client): - mock_build.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_k8s_failure(mock_create, client): mock_create.side_effect = RuntimeError("apiserver down") rv = client.post( "/api/run", @@ -180,9 +186,8 @@ def test_api_run_k8s_failure(mock_build, mock_create, client): assert "apiserver down" in rv.get_json()["error"] -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_ignores_non_pull_request(mock_build_pr, mock_create, client): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_ignores_non_pull_request(mock_create, client): rv = client.post( "/webhook/github", data=json.dumps({"action": "created"}), @@ -192,12 +197,10 @@ def test_webhook_ignores_non_pull_request(mock_build_pr, mock_create, client): assert rv.status_code == 200 assert rv.get_json()["status"] == "ignored" mock_create.assert_not_called() - mock_build_pr.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_unknown_repo_ignored(mock_build_pr, mock_create, client, flask_app): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_unknown_repo_ignored(mock_create, client, flask_app): flask_app.config["RESOLVER"].resolve_repo.return_value = None payload = _pr_payload("opened", repo_name="unknown-repo", pr_number=7) rv = client.post( @@ -211,9 +214,8 @@ def test_webhook_unknown_repo_ignored(mock_build_pr, mock_create, client, flask_ mock_create.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_opened_creates_pr(mock_build_pr, mock_create, client, flask_app): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_opened_creates_pr(mock_create, client, flask_app): flask_app.config["RESOLVER"].resolve_repo.return_value = { "stack_file": "stacks/s.yaml", "app_name": "fe", @@ -229,16 +231,15 @@ def test_webhook_opened_creates_pr(mock_build_pr, mock_create, client, flask_app ) assert rv.status_code == 200 assert rv.get_json()["pipelinerun"] == "webhook-pr" - mock_build_pr.assert_called_once() - call_kw = mock_build_pr.call_args.kwargs - assert call_kw["changed_app"] == "fe" - assert call_kw["pr_number"] == 3 - assert '"fe"' in call_kw["app_revisions"] and "abc123" in call_kw["app_revisions"] + assert rv.get_json()["stackrun"] == "webhook-pr" + spec = _created_spec(mock_create) + assert spec["changedApp"] == "fe" + assert spec["prNumber"] == 3 + assert '"fe"' in spec["appRevisions"] and "abc123" in spec["appRevisions"] -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_synchronize_creates_pr(mock_build_pr, mock_create, client, flask_app): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_synchronize_creates_pr(mock_create, client, flask_app): mock_create.return_value = "sync-run" payload = _pr_payload("synchronize", repo_name="demo-fe", pr_number=9, head_sha="sha") rv = client.post( @@ -248,12 +249,11 @@ def test_webhook_synchronize_creates_pr(mock_build_pr, mock_create, client, flas headers={"X-GitHub-Event": "pull_request"}, ) assert rv.status_code == 200 - mock_build_pr.assert_called_once() + assert _created_spec(mock_create)["prNumber"] == 9 -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_merge_pipelinerun") -def test_webhook_closed_merged_creates_merge(mock_build_merge, mock_create, client): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_closed_merged_creates_merge(mock_create, client): mock_create.return_value = "merge-run" payload = _pr_payload( "closed", @@ -269,13 +269,11 @@ def test_webhook_closed_merged_creates_merge(mock_build_merge, mock_create, clie ) assert rv.status_code == 200 assert rv.get_json()["mode"] == "merge" - mock_build_merge.assert_called_once() - assert mock_build_merge.call_args.kwargs["git_revision"] == "main" + assert _created_spec(mock_create)["gitRevision"] == "main" -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_merge_pipelinerun") -def test_webhook_closed_not_merged_ignored(mock_build_merge, mock_create, client): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_closed_not_merged_ignored(mock_create, client): payload = _pr_payload("closed", repo_name="demo-fe", pr_number=2, merged=False) rv = client.post( "/webhook/github", @@ -285,12 +283,11 @@ def test_webhook_closed_not_merged_ignored(mock_build_merge, mock_create, client ) assert rv.status_code == 200 assert rv.get_json()["status"] == "ignored" - mock_build_merge.assert_not_called() + mock_create.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_pr_create_failure_500(mock_build_pr, mock_create, client): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_pr_create_failure_500(mock_create, client): mock_create.side_effect = OSError("network") payload = _pr_payload("opened", repo_name="demo-fe", pr_number=1, head_sha="s") rv = client.post( @@ -431,10 +428,8 @@ def test_create_app_env_overrides(monkeypatch, tmp_path): assert app.config["MAX_RETRIES"] == 3 -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_promote_pipelinerun") -def test_api_run_promote_success(mock_build_promote, mock_create, client): - mock_build_promote.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_promote_success(mock_create, client): mock_create.return_value = "promote-1" rv = client.post( "/api/run", @@ -451,15 +446,13 @@ def test_api_run_promote_success(mock_build_promote, mock_create, client): ) assert rv.status_code == 200 assert rv.get_json()["mode"] == "promote" - mock_build_promote.assert_called_once() - assert mock_build_promote.call_args.kwargs["release_version"] == "0.1.0" - assert mock_build_promote.call_args.kwargs["changed_app"] == "demo-fe" + spec = _created_spec(mock_create) + assert spec["releaseVersion"] == "0.1.0" + assert spec["changedApp"] == "demo-fe" -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_promote_pipelinerun") -def test_api_run_promote_with_approval(mock_build_promote, mock_create, client): - mock_build_promote.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_promote_with_approval(mock_create, client): mock_create.return_value = "promote-approved" rv = client.post( "/api/run", @@ -478,17 +471,15 @@ def test_api_run_promote_with_approval(mock_build_promote, mock_create, client): content_type="application/json", ) assert rv.status_code == 200 - kw = mock_build_promote.call_args.kwargs - assert kw["require_approval"] is True - assert kw["approved_by"] == "alice@example.com" - assert kw["timeout"] == "30m" - assert kw["max_retries"] == 1 + spec = _created_spec(mock_create) + assert spec["requireApproval"] is True + assert spec["approvedBy"] == "alice@example.com" + assert spec["timeout"] == "30m" + assert spec["maxRetries"] == 1 -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_api_run_pr_forwards_reliability(mock_build_pr, mock_create, client): - mock_build_pr.return_value = {} +@patch("routes.k8s_client.create_stackrun") +def test_api_run_pr_forwards_reliability(mock_create, client): mock_create.return_value = "pr-rel" rv = client.post( "/api/run", @@ -504,24 +495,26 @@ def test_api_run_pr_forwards_reliability(mock_build_pr, mock_create, client): content_type="application/json", ) assert rv.status_code == 200 - kw = mock_build_pr.call_args.kwargs - assert kw["timeout"] == "15m" - assert kw["max_retries"] == 0 + spec = _created_spec(mock_create) + assert spec["timeout"] == "15m" + assert spec["maxRetries"] == 0 -@patch("routes.builder.build_promote_pipelinerun") -def test_api_run_promote_requires_fields(mock_build_promote, client): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_promote_requires_fields(mock_create, client): rv = client.post( "/api/run", data=json.dumps({"mode": "promote", "release_version": "0.1.0"}), content_type="application/json", ) assert rv.status_code == 400 - mock_build_promote.assert_not_called() + mock_create.assert_not_called() -@patch("routes.builder.build_promote_pipelinerun") -def test_api_run_promote_requires_approval_actor(mock_build_promote, client): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_promote_without_approved_by_is_pending_approval(mock_create, client): + """require_approval without approved_by creates a StackRun; operator waits.""" + mock_create.return_value = "stackrun-promote-wait" rv = client.post( "/api/run", data=json.dumps( @@ -535,14 +528,14 @@ def test_api_run_promote_requires_approval_actor(mock_build_promote, client): ), content_type="application/json", ) - assert rv.status_code == 400 - assert "approved_by" in rv.get_json()["error"] - mock_build_promote.assert_not_called() + assert rv.status_code == 200, rv.get_json() + spec = _created_spec(mock_create) + assert spec["requireApproval"] is True + assert "approvedBy" not in spec -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_rejects_invalid_signature(mock_build_pr, mock_create, client, flask_app): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_rejects_invalid_signature(mock_create, client, flask_app): flask_app.config["WEBHOOK_SECRET"] = "s3cr3t" payload = _pr_payload("opened", repo_name="demo-fe", pr_number=1) rv = client.post( @@ -559,10 +552,9 @@ def test_webhook_rejects_invalid_signature(mock_build_pr, mock_create, client, f @patch("routes.k8s_client.get_secret_data") -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") +@patch("routes.k8s_client.create_stackrun") def test_webhook_fail_closed_when_named_secret_missing( - mock_build_pr, mock_create, mock_get_secret, client, flask_app + mock_create, mock_get_secret, client, flask_app ): flask_app.config["WEBHOOK_SECRET"] = "" flask_app.config["WEBHOOK_SECRET_NAME"] = "github-webhook-secret" @@ -579,9 +571,8 @@ def test_webhook_fail_closed_when_named_secret_missing( mock_create.assert_not_called() -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_accepts_valid_signature(mock_build_pr, mock_create, client, flask_app): +@patch("routes.k8s_client.create_stackrun") +def test_webhook_accepts_valid_signature(mock_create, client, flask_app): import webhook_auth flask_app.config["WEBHOOK_SECRET"] = "s3cr3t" @@ -603,10 +594,9 @@ def test_webhook_accepts_valid_signature(mock_build_pr, mock_create, client, fla @patch("routes.k8s_client.get_secret_data") -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_pr_pipelinerun") +@patch("routes.k8s_client.create_stackrun") def test_webhook_accepts_valid_signature_from_named_secret( - mock_build_pr, mock_create, mock_get_secret, client, flask_app + mock_create, mock_get_secret, client, flask_app ): import webhook_auth @@ -670,16 +660,12 @@ def test_injection_status_k8s_error_is_503(mock_secrets, mock_cms, client, flask assert "kubernetes lookup failed" in rv.get_json()["error"] -@patch("routes.k8s_client.create_pipelinerun") -@patch("routes.builder.build_promote_pipelinerun") -def test_api_run_promote_resolves_registries_file( - mock_build_promote, mock_create, client, flask_app -): +@patch("routes.k8s_client.create_stackrun") +def test_api_run_promote_resolves_registries_file(mock_create, client, flask_app): from pathlib import Path registries = Path(__file__).resolve().parents[2] / "stacks" / "registries.yaml" flask_app.config["REGISTRIES_FILE"] = str(registries) - mock_build_promote.return_value = {} mock_create.return_value = "promote-from-file" rv = client.post( "/api/run", @@ -694,9 +680,9 @@ def test_api_run_promote_resolves_registries_file( content_type="application/json", ) assert rv.status_code == 200 - kw = mock_build_promote.call_args.kwargs - assert kw["target_registry"] == "localhost:5002" - assert kw["credentials_secret"] == "registry-prod-creds" + spec = _created_spec(mock_create) + assert spec["targetRegistry"] == "localhost:5002" + assert spec["credentialsSecret"] == "registry-prod-creds" @patch("routes.k8s_client.list_configmap_names") diff --git a/orchestrator/tests/test_stackrun_crd_path.py b/orchestrator/tests/test_stackrun_crd_path.py index 74b0890..ddc1443 100644 --- a/orchestrator/tests/test_stackrun_crd_path.py +++ b/orchestrator/tests/test_stackrun_crd_path.py @@ -1,4 +1,4 @@ -"""STACKRUN_VIA_CRD=true creates StackRun CRs instead of PipelineRuns.""" +"""Flask always creates StackRun CRs (M16: STACKRUN_VIA_CRD hatch retired).""" import json from unittest.mock import patch @@ -6,8 +6,7 @@ @patch("routes.k8s_client.create_stackrun") @patch("routes.k8s_client.create_pipelinerun") -def test_api_run_bootstrap_via_crd(mock_pr, mock_sr, client, flask_app): - flask_app.config["STACKRUN_VIA_CRD"] = True +def test_api_run_bootstrap_creates_stackrun(mock_pr, mock_sr, client): mock_sr.return_value = "stackrun-bootstrap-abc12" rv = client.post( "/api/run", @@ -28,10 +27,7 @@ def test_api_run_bootstrap_via_crd(mock_pr, mock_sr, client, flask_app): @patch("routes.k8s_client.create_stackrun") -@patch("routes.builder.build_pr_pipelinerun") -def test_webhook_via_crd(mock_build_pr, mock_sr, client, flask_app): - flask_app.config["STACKRUN_VIA_CRD"] = True - mock_build_pr.return_value = {"kind": "PipelineRun"} +def test_webhook_creates_stackrun(mock_sr, client): mock_sr.return_value = "stackrun-pr-xyz" payload = { "action": "opened", @@ -55,9 +51,8 @@ def test_webhook_via_crd(mock_build_pr, mock_sr, client, flask_app): @patch("routes.k8s_client.create_stackrun") @patch("routes.k8s_client.create_pipelinerun") -def test_api_run_promote_via_crd_waits_for_approval(mock_pr, mock_sr, client, flask_app): +def test_api_run_promote_without_approved_by_creates_stackrun(mock_pr, mock_sr, client): """CRD path matches GUI: require_approval without approved_by is PendingApproval.""" - flask_app.config["STACKRUN_VIA_CRD"] = True mock_sr.return_value = "stackrun-promote-wait" rv = client.post( "/api/run", @@ -79,3 +74,13 @@ def test_api_run_promote_via_crd_waits_for_approval(mock_pr, mock_sr, client, fl spec = mock_sr.call_args.args[0]["spec"] assert spec["requireApproval"] is True assert "approvedBy" not in spec + + +def test_create_app_ignores_stackrun_via_crd_false(monkeypatch, tmp_path): + monkeypatch.setenv("STACKRUN_VIA_CRD", "false") + monkeypatch.setenv("STACKS_DIR", str(tmp_path)) + monkeypatch.setenv("TEAMS_DIR", str(tmp_path)) + from app import create_app + + app = create_app() + assert "STACKRUN_VIA_CRD" not in app.config diff --git a/scripts/generate-run.sh b/scripts/generate-run.sh index 8da903e..cc92213 100755 --- a/scripts/generate-run.sh +++ b/scripts/generate-run.sh @@ -3,10 +3,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # generate-run.sh — Generate and optionally apply a StackRun (operator) -# for a given stack. Pass --pipeline-run to emit a raw Tekton PipelineRun. +# for a given stack. Raw PipelineRun emit (--pipeline-run) was removed in M16. -# a given stack, triggered either as a PR test or a merge release. -# # Usage: # ./generate-run.sh --mode pr --repo demo-fe --pr 42 # ./generate-run.sh --mode merge --repo demo-fe @@ -30,9 +28,8 @@ source "$SCRIPT_DIR/common.sh" # --namespace Target namespace (default: tekton-pipelines) # --storage-class PVC storage class # --intercept-backend telepresence (default) | mirrord (M7) -# --apply kubectl create the StackRun (or PipelineRun with --pipeline-run) +# --apply kubectl create the StackRun # --dry-run Print the YAML without applying -# --pipeline-run Emit a Tekton PipelineRun instead of a StackRun REGISTRY_FILE="$STACKS_DIR/registry.yaml" @@ -52,7 +49,10 @@ APPLY=false BUILD_IMAGES="${BUILD_IMAGES:-true}" BUILD_IMAGE_TAG="${BUILD_IMAGE_TAG:-latest}" INTERCEPT_BACKEND="${INTERCEPT_BACKEND:-telepresence}" -EMIT_PIPELINERUN="${GENERATE_PIPELINE_RUN:-false}" + +if [[ "${GENERATE_PIPELINE_RUN:-false}" == "true" ]]; then + die "--pipeline-run / GENERATE_PIPELINE_RUN was removed in M16; emit a StackRun (default) instead" +fi while [[ $# -gt 0 ]]; do case "$1" in @@ -75,7 +75,7 @@ while [[ $# -gt 0 ]]; do --intercept-backend) INTERCEPT_BACKEND="$2"; shift 2 ;; --apply) APPLY=true; shift ;; --dry-run) APPLY=false; shift ;; - --pipeline-run) EMIT_PIPELINERUN=true; shift ;; + --pipeline-run) die "--pipeline-run was removed in M16; emit a StackRun (default) instead" ;; *) die "Unknown option: $1" ;; esac done @@ -111,16 +111,15 @@ fi STACK_REF="${STACK%.yaml}" STACK_REF="${STACK_REF%.yml}" -if [[ "$EMIT_PIPELINERUN" != "true" ]]; then - if [[ "$MODE" == "pr" ]]; then - [[ -n "$PR" ]] || die "--pr is required for pr mode" - [[ -n "$APP" ]] || die "PR mode tests one app at a time: --app is required (e.g. --app demo-fe)" - PR_REPO_URL="" - if [[ "$APP_REVISIONS" != "{}" && -n "$APP" ]]; then - REPO_SLUG=$(yq -r ".apps[] | select(.name == \"$APP\") | .repo" "$STACKS_DIR/$STACK" 2>/dev/null || true) - [[ -n "$REPO_SLUG" && "$REPO_SLUG" != "null" ]] && PR_REPO_URL="https://github.com/${REPO_SLUG}.git" - fi - cat </dev/null || true) + [[ -n "$REPO_SLUG" && "$REPO_SLUG" != "null" ]] && PR_REPO_URL="https://github.com/${REPO_SLUG}.git" + fi + cat </dev/null || true) - [[ -n "$REPO_SLUG" && "$REPO_SLUG" != "null" ]] && PR_REPO_URL="https://github.com/${REPO_SLUG}.git" - fi - - cat </dev/null 2>&1; then + echo " FAIL: --pipeline-run should exit nonzero (removed in M16)" + ((ERRORS++)) || true +else + echo " OK: --pipeline-run is rejected" +fi echo "" if [[ $ERRORS -gt 0 ]]; then