diff --git a/deploy/templates/tekton/pipeline.yaml b/deploy/templates/tekton/pipeline.yaml index 0e36c96..1a81338 100644 --- a/deploy/templates/tekton/pipeline.yaml +++ b/deploy/templates/tekton/pipeline.yaml @@ -465,3 +465,5 @@ spec: value: $(tasks.image-publish.results.env-identity) - name: image-status value: $(tasks.image-publish.results.status) + - name: tools-image + value: $(params.tools-image) diff --git a/src/selftest/cases/80-cicd-tekton-contract.ts b/src/selftest/cases/80-cicd-tekton-contract.ts new file mode 100644 index 0000000..962bc71 --- /dev/null +++ b/src/selftest/cases/80-cicd-tekton-contract.ts @@ -0,0 +1,84 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import assert from "node:assert/strict"; +import type { SelfTestCase } from "../harness.js"; + +type InlineTaskSpecContract = { + taskName: string; + requiredParams: string[]; + providedParams: string[]; +}; + +const selfTest: SelfTestCase = async (context) => { + const text = await readFile(path.join(context.root, "deploy/templates/tekton/pipeline.yaml"), "utf8"); + const tasks = inlineTaskSpecContracts(text); + + for (const task of tasks) { + const provided = new Set(task.providedParams); + + for (const name of task.requiredParams) { + assert.equal(provided.has(name), true, `pipeline task ${task.taskName} must pass taskSpec param ${name}`); + } + } + + return { name: "80-cicd-tekton-contract", tests: ["inline taskSpec params are passed by every pipeline task"] }; +}; + +function inlineTaskSpecContracts(text: string): InlineTaskSpecContract[] { + const tasks: InlineTaskSpecContract[] = []; + let current: InlineTaskSpecContract | null = null; + let inTasks = false; + let inTaskSpec = false; + let section: "taskSpecParams" | "taskParams" | null = null; + + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.replace(/\s+$/u, ""); + const trimmed = line.trim(); + const indent = line.length - line.trimStart().length; + + if (line === " tasks:") { + inTasks = true; + continue; + } + if (!inTasks || trimmed.length === 0 || trimmed.startsWith("#")) continue; + + const taskMatch = /^ - name: (.+)$/u.exec(line); + if (taskMatch) { + if (current) tasks.push(current); + current = { taskName: taskMatch[1] ?? "", requiredParams: [], providedParams: [] }; + inTaskSpec = false; + section = null; + continue; + } + if (!current) continue; + + if (indent === 6 && trimmed === "taskSpec:") { + inTaskSpec = true; + section = null; + continue; + } + if (indent === 6 && trimmed === "params:") { + inTaskSpec = false; + section = "taskParams"; + continue; + } + if (indent === 8 && inTaskSpec && trimmed === "params:") { + section = "taskSpecParams"; + continue; + } + if ((section === "taskSpecParams" && indent <= 8) || (section === "taskParams" && indent <= 6)) { + section = null; + } + + const paramMatch = /^\s*- name: (.+)$/u.exec(line); + if (!paramMatch || !section) continue; + const name = paramMatch[1] ?? ""; + if (section === "taskSpecParams") current.requiredParams.push(name); + if (section === "taskParams") current.providedParams.push(name); + } + + if (current) tasks.push(current); + return tasks; +} + +export default selfTest;