Merge remote-tracking branch 'origin/master' into feat/2010-hwlab-release-production-lane

This commit is contained in:
Codex
2026-07-15 10:46:53 +02:00
53 changed files with 1791 additions and 44 deletions
+18
View File
@@ -441,6 +441,24 @@ function agentRunTektonGitopsPublishScript(spec: AgentRunLaneSpec): string {
"cd \"$root/gitops\"",
"git fetch origin \"$(params.gitops-branch)\" || true",
"if git rev-parse --verify \"refs/remotes/origin/$(params.gitops-branch)^{commit}\" >/dev/null 2>&1; then git checkout -B \"$(params.gitops-branch)\" \"refs/remotes/origin/$(params.gitops-branch)\"; else git checkout --orphan \"$(params.gitops-branch)\"; git rm -rf . >/dev/null 2>&1 || true; fi",
"incoming_source_commit='$(params.revision)'",
"source_branch='$(params.source-branch)'",
"branch_source_commit=$(git -C \"$root/repo\" rev-parse --verify \"refs/remotes/origin/${source_branch}^{commit}\")",
"gitops_source_commit=$(node -e 'try { const value = JSON.parse(require(\"node:fs\").readFileSync(\"source.json\", \"utf8\")).sourceCommit; if (typeof value === \"string\") process.stdout.write(value); } catch {}')",
"stale_reason=''",
"if [ \"$incoming_source_commit\" != \"$branch_source_commit\" ]; then stale_reason='stale-source-revision'; fi",
"if [ -z \"$stale_reason\" ] && [ -n \"$gitops_source_commit\" ] && [ \"$incoming_source_commit\" != \"$gitops_source_commit\" ]; then",
" if ! git -C \"$root/repo\" cat-file -e \"${gitops_source_commit}^{commit}\" 2>/dev/null; then stale_reason='gitops-source-order-unverifiable';",
" elif git -C \"$root/repo\" merge-base --is-ancestor \"$incoming_source_commit\" \"$gitops_source_commit\"; then stale_reason='stale-gitops-source-revision';",
" elif ! git -C \"$root/repo\" merge-base --is-ancestor \"$gitops_source_commit\" \"$incoming_source_commit\"; then stale_reason='divergent-gitops-source-revision'; fi",
"fi",
"if [ -n \"$stale_reason\" ]; then",
" gitops_commit=$(git rev-parse HEAD)",
" INCOMING_SOURCE_COMMIT=\"$incoming_source_commit\" CURRENT_SOURCE_COMMIT=\"$branch_source_commit\" GITOPS_SOURCE_COMMIT=\"$gitops_source_commit\" GITOPS_COMMIT=\"$gitops_commit\" STALE_REASON=\"$stale_reason\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, status: 'succeeded', phase: 'gitops-publish', reason: process.env.STALE_REASON, warning: true, blocking: false, changed: false, stale: true, gitopsCommit: process.env.GITOPS_COMMIT, sourceCommit: process.env.INCOMING_SOURCE_COMMIT, currentSourceCommit: process.env.CURRENT_SOURCE_COMMIT, gitopsSourceCommit: process.env.GITOPS_SOURCE_COMMIT || null, valuesPrinted: false }));",
"NODE",
" exit 0",
"fi",
"git rm -rf --ignore-unmatch \"$(params.gitops-root)\" \"$(params.artifact-catalog)\" source.json >/dev/null 2>&1 || true",
"rm -rf \"$(params.gitops-root)\" \"$(params.artifact-catalog)\" source.json",
"TEMPLATES_B64=\"$templates_b64\" BUILD_RESULT=\"$build_result\" node <<'NODE'",
@@ -21,11 +21,13 @@ test("shared stage injects owning YAML OTel values for all three PaC repositorie
renderedRootExpression: "'/workspace/rendered'",
});
expect(stage).toContain("feature-config-schema-validation");
expect(stage).toContain("cicd.feature_config.schema");
expect(stage).toContain("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces'");
expect(stage).toContain("OTEL_TRACES_SAMPLER_ARG='1'");
expect(stage).toContain("OTEL_EXPORTER_TIMEOUT_MS='300'");
expect(stage).toContain("NODE_UNIDESK_AJV2020_BUNDLE");
expect(stage).toContain("base64 -d | gzip -d");
expect(stage).toContain("NODE_UNIDESK_FEATURE_CONFIG_SCHEMA");
expect(stage).not.toContain("cicd.feature_config.schema");
expect(stage).not.toContain("NODE_UNIDESK_AJV2020_BUNDLE");
expect(stage).not.toContain("process.env.webProbeSentinel");
}
});
@@ -112,7 +114,9 @@ test("AgentRun normal GitOps publish task contains the shared warning-only stage
expect(scripts).toContain("feature-config-schema-validation");
expect(scripts).toContain("feature-config-validator-process-failure");
expect(scripts).toContain("OTEL_EXPORTER_TIMEOUT_MS='300'");
expect(scripts).toContain("NODE_UNIDESK_AJV2020_BUNDLE");
expect(scripts).toContain("base64 -d | gzip -d");
expect(scripts).toContain("NODE_UNIDESK_FEATURE_CONFIG_SCHEMA");
expect(scripts).not.toContain("NODE_UNIDESK_AJV2020_BUNDLE");
});
test("repo-owned schema is Draft 2020-12 with one canonical feature property", () => {
+23 -10
View File
@@ -1,10 +1,13 @@
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
import { rootPath } from "./config";
import { loadCicdOtelRuntimeConfig } from "../native/cicd/otel-runtime-config";
const validatorSource = readFileSync(rootPath("scripts/native/cicd/feature-config-schema-warning.mjs"), "utf8").trimEnd();
const ajv2020BundleSource = readFileSync(rootPath("scripts/vendor/ajv-dist/8.17.1/ajv2020.min.js"), "utf8").trimEnd();
const validatorGzipBase64 = gzipBase64(validatorSource);
const ajv2020GzipBase64 = gzipBase64(ajv2020BundleSource);
export interface PacFeatureConfigSchemaStageOptions {
readonly repositoryId: string;
@@ -26,14 +29,21 @@ export function renderPacFeatureConfigSchemaStage(options: PacFeatureConfigSchem
`export OTEL_SERVICE_NAME=${shellSingle(otel.serviceName)}`,
`export OTEL_TRACES_SAMPLER_ARG=${shellSingle(String(otel.samplingRatio))}`,
`export OTEL_EXPORTER_TIMEOUT_MS=${shellSingle(String(otel.timeoutMs))}`,
"feature_config_validator_base=''",
"feature_config_validator=''",
"if feature_config_validator_base=\"$(mktemp \"${TMPDIR:-/tmp}/unidesk-feature-config-validator.XXXXXX\" 2>/dev/null)\"; then",
" feature_config_validator=\"${feature_config_validator_base}.mjs\"",
" if ! printf '%s' " + shellSingle(validatorGzipBase64) + " | base64 -d | gzip -d >\"$feature_config_validator\"; then",
" rm -f \"$feature_config_validator_base\" \"$feature_config_validator\" || true",
" feature_config_validator=''",
" fi",
" rm -f \"$feature_config_validator_base\" || true",
"fi",
...(materializeBundle
? [
"feature_config_ajv_bundle=''",
"if feature_config_ajv_bundle=\"$(mktemp \"${TMPDIR:-/tmp}/unidesk-ajv2020.XXXXXX\" 2>/dev/null)\"; then",
" if ! cat >\"$feature_config_ajv_bundle\" <<'NODE_UNIDESK_AJV2020_BUNDLE'",
ajv2020BundleSource,
"NODE_UNIDESK_AJV2020_BUNDLE",
" then",
" if ! printf '%s' " + shellSingle(ajv2020GzipBase64) + " | base64 -d | gzip -d >\"$feature_config_ajv_bundle\"; then",
" rm -f \"$feature_config_ajv_bundle\" || true",
" feature_config_ajv_bundle=''",
" fi",
@@ -41,17 +51,16 @@ export function renderPacFeatureConfigSchemaStage(options: PacFeatureConfigSchem
"export UNIDESK_AJV2020_BUNDLE=\"${feature_config_ajv_bundle:-${TMPDIR:-/tmp}/unidesk-ajv2020-unavailable}\"",
]
: [`export UNIDESK_AJV2020_BUNDLE=${options.ajvBundleExpression}`]),
"export UNIDESK_FEATURE_CONFIG_VALIDATOR=\"${feature_config_validator:-${TMPDIR:-/tmp}/unidesk-feature-config-validator-unavailable.mjs}\"",
"if ! node --input-type=module <<'NODE_UNIDESK_FEATURE_CONFIG_SCHEMA'",
validatorSource,
"await runFeatureConfigSchemaValidation({",
" repoDir: process.env.UNIDESK_FEATURE_CONFIG_REPO_DIR,",
" renderedRoot: process.env.UNIDESK_FEATURE_CONFIG_RENDERED_ROOT,",
" consumer: process.env.UNIDESK_PAC_CONSUMER,",
"});",
"import { pathToFileURL } from 'node:url';",
"const validator = await import(pathToFileURL(process.env.UNIDESK_FEATURE_CONFIG_VALIDATOR).href);",
"await validator.runFeatureConfigSchemaValidation({ repoDir: process.env.UNIDESK_FEATURE_CONFIG_REPO_DIR, renderedRoot: process.env.UNIDESK_FEATURE_CONFIG_RENDERED_ROOT, consumer: process.env.UNIDESK_PAC_CONSUMER });",
"NODE_UNIDESK_FEATURE_CONFIG_SCHEMA",
"then",
" printf '{\"event\":\"feature-config-schema-validation\",\"warning\":true,\"blocking\":false,\"code\":\"feature-config-validator-process-failure\",\"mutation\":false,\"valuesPrinted\":false}\\n' >&2",
"fi",
"if [ -n \"$feature_config_validator\" ]; then rm -f \"$feature_config_validator\" || true; fi",
...(materializeBundle ? ["if [ -n \"$feature_config_ajv_bundle\" ]; then rm -f \"$feature_config_ajv_bundle\" || true; fi"] : []),
"if command -v ci_timing_emit >/dev/null 2>&1; then",
" ci_timing_emit feature-config-schema-validation succeeded \"$feature_config_schema_started_ms\"",
@@ -64,3 +73,7 @@ export function renderPacFeatureConfigSchemaStage(options: PacFeatureConfigSchem
function shellSingle(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function gzipBase64(value: string): string {
return gzipSync(Buffer.from(value, "utf8"), { level: 9 }).toString("base64");
}
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
import { renderMirrorBootstrap } from "./platform-infra-gitea-render";
import { pacBootstrapYamlMatchFailure, projectPacBootstrapResult, renderPacBootstrap, resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { parsePacConfigDocument, readPacConfig, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
import { parsePacConfigDocument, readPacConfig, renderPacReleaseManifest, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
import { materializeYamlComposition } from "./yaml-composition";
const mirror: GiteaMirrorRepository = {
@@ -185,6 +185,38 @@ test("PaC apply checks the Gitea repository before dry-run return and cluster mu
expect(statusAction).toContain("blocking: false");
});
test("PaC release manifest renders YAML-owned watcher startup backlog probes", () => {
const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true });
const manifest = renderPacReleaseManifest(pac, `apiVersion: apps/v1
kind: Deployment
metadata:
name: pipelines-as-code-watcher
namespace: pipelines-as-code
spec:
template:
spec:
containers:
- name: pac-watcher
image: example.invalid/pac-watcher:v0
readinessProbe:
httpGet:
path: /live
port: probes
---
apiVersion: v1
kind: Service
metadata:
name: pipelines-as-code-watcher
namespace: pipelines-as-code
`);
const documents = manifest.split(/^---\s*$/mu).map((item) => item.trim()).filter(Boolean).map((item) => Bun.YAML.parse(item) as any);
const watcher = documents[0].spec.template.spec.containers[0];
expect(watcher.startupProbe).toEqual(pac.release.workloads.watcher.startupProbe);
expect(watcher.readinessProbe).toEqual(pac.release.workloads.watcher.readinessProbe);
expect(watcher.livenessProbe).toEqual(pac.release.workloads.watcher.livenessProbe);
expect(documents[1].kind).toBe("Service");
});
test("platform bootstrap help advertises the composite entry before low-level apply", async () => {
const help = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["help", "platform-bootstrap"]);
const usage = (help as Record<string, unknown>).usage as string[];
@@ -71,6 +71,26 @@ interface PacTarget {
enabled: boolean;
}
interface PacHttpProbe {
httpGet: {
path: string;
port: string;
};
initialDelaySeconds: number;
periodSeconds: number;
timeoutSeconds: number;
failureThreshold: number;
successThreshold: number;
}
interface PacWorkloadProbes {
deploymentName: string;
containerName: string;
startupProbe: PacHttpProbe;
readinessProbe: PacHttpProbe;
livenessProbe: PacHttpProbe;
}
export interface PacConfig {
version: number;
kind: "platform-infra-pipelines-as-code";
@@ -104,6 +124,9 @@ export interface PacConfig {
controllerServiceName: string;
controllerServicePort: number;
waitTimeoutSeconds: number;
workloads: {
watcher: PacWorkloadProbes;
};
};
deliveryProvenance: {
version: string;
@@ -474,6 +497,8 @@ export function parsePacConfigDocument(
selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean } = {},
): PacConfig {
const release = y.objectField(root, "release", "");
const releaseWorkloads = y.objectField(release, "workloads", "release");
const watcherWorkload = y.objectField(releaseWorkloads, "watcher", "release.workloads");
const deliveryProvenance = y.objectField(root, "deliveryProvenance", "");
const provenanceController = y.objectField(deliveryProvenance, "controller", "deliveryProvenance");
const provenanceAdmission = y.objectField(deliveryProvenance, "admission", "deliveryProvenance");
@@ -542,6 +567,9 @@ export function parsePacConfigDocument(
controllerServiceName: y.kubernetesNameField(release, "controllerServiceName", "release"),
controllerServicePort: y.portField(release, "controllerServicePort", "release"),
waitTimeoutSeconds: positiveInteger(release, "waitTimeoutSeconds", "release"),
workloads: {
watcher: parsePacWorkloadProbes(watcherWorkload, "release.workloads.watcher"),
},
},
deliveryProvenance: {
version: y.stringField(deliveryProvenance, "version", "deliveryProvenance"),
@@ -1714,7 +1742,35 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
if (!response.ok) throw new Error(`failed to fetch ${pac.release.manifestUrl}: HTTP ${response.status}`);
const text = await response.text();
if (!text.includes("repositories.pipelinesascode.tekton.dev")) throw new Error(`${pac.release.manifestUrl} did not contain the Repository CRD`);
return text;
return renderPacReleaseManifest(pac, text);
}
export function renderPacReleaseManifest(pac: PacConfig, source: string): string {
const workload = pac.release.workloads.watcher;
let matched = false;
const documents = source
.split(/^---\s*$/mu)
.map((item) => item.trim())
.filter(Boolean)
.map((item) => Bun.YAML.parse(item) as unknown)
.map((item) => {
const document = record(item);
const metadata = record(document.metadata);
if (document.kind !== "Deployment" || metadata.name !== workload.deploymentName || metadata.namespace !== pac.release.namespace) return document;
const spec = record(document.spec);
const template = record(spec.template);
const podSpec = record(template.spec);
const containers = arrayRecords(podSpec.containers);
const container = containers.find((candidate) => candidate.name === workload.containerName);
if (container === undefined) throw new Error(`${configLabel}.release.workloads.watcher.containerName did not match ${workload.deploymentName}`);
container.startupProbe = structuredClone(workload.startupProbe);
container.readinessProbe = structuredClone(workload.readinessProbe);
container.livenessProbe = structuredClone(workload.livenessProbe);
matched = true;
return document;
});
if (!matched) throw new Error(`${configLabel}.release.workloads.watcher.deploymentName did not match the release manifest`);
return `${documents.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`;
}
function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
@@ -3187,6 +3243,31 @@ function positiveInteger(obj: Record<string, unknown>, key: string, path: string
return value;
}
function parsePacHttpProbe(obj: Record<string, unknown>, path: string): PacHttpProbe {
const httpGet = y.objectField(obj, "httpGet", path);
return {
httpGet: {
path: y.stringField(httpGet, "path", `${path}.httpGet`),
port: y.stringField(httpGet, "port", `${path}.httpGet`),
},
initialDelaySeconds: positiveInteger(obj, "initialDelaySeconds", path),
periodSeconds: positiveInteger(obj, "periodSeconds", path),
timeoutSeconds: positiveInteger(obj, "timeoutSeconds", path),
failureThreshold: positiveInteger(obj, "failureThreshold", path),
successThreshold: positiveInteger(obj, "successThreshold", path),
};
}
function parsePacWorkloadProbes(obj: Record<string, unknown>, path: string): PacWorkloadProbes {
return {
deploymentName: y.kubernetesNameField(obj, "deploymentName", path),
containerName: y.kubernetesNameField(obj, "containerName", path),
startupProbe: parsePacHttpProbe(y.objectField(obj, "startupProbe", path), `${path}.startupProbe`),
readinessProbe: parsePacHttpProbe(y.objectField(obj, "readinessProbe", path), `${path}.readinessProbe`),
livenessProbe: parsePacHttpProbe(y.objectField(obj, "livenessProbe", path), `${path}.livenessProbe`),
};
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
const parsed = new URL(value);