fix: share selfmedia PaC repository across lanes
This commit is contained in:
@@ -211,41 +211,6 @@ repositories:
|
||||
runtime_service_port: "4317"
|
||||
health_path: /healthz
|
||||
health_url: http://selfmedia.selfmedia.svc.cluster.local:4317/healthz
|
||||
- id: selfmedia-production-nc01
|
||||
name: selfmedia-production-nc01
|
||||
namespace: selfmedia-production-ci
|
||||
providerType: gitea
|
||||
url: https://gitea.pikapython.com/mirrors/pikainc-selfmedia
|
||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git
|
||||
owner: mirrors
|
||||
repo: pikainc-selfmedia
|
||||
secretName: pac-gitea-selfmedia-production-nc01
|
||||
tokenKey: token
|
||||
webhookSecretKey: webhook.secret
|
||||
concurrencyLimit: 1
|
||||
params:
|
||||
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git
|
||||
source_branch: release
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
||||
node: NC01
|
||||
pipeline_name: selfmedia-production-nc01-pac
|
||||
pipeline_run_prefix: selfmedia-production-nc01
|
||||
service_account: selfmedia-production-nc01-tekton-runner
|
||||
pipeline_timeout: 2h0m0s
|
||||
image_repository: 127.0.0.1:5000/selfmedia/newsroom-production
|
||||
registry_probe_base: http://127.0.0.1:5000
|
||||
gitops_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git
|
||||
gitops_write_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git
|
||||
gitops_branch: nc01-selfmedia-production-gitops
|
||||
gitops_username: unidesk-admin
|
||||
gitops_secret_name: pac-gitea-selfmedia-production-nc01
|
||||
gitops_manifest_path: deploy/gitops/nc01-production/resources.yaml
|
||||
runtime_namespace: selfmedia-production
|
||||
runtime_deployment: selfmedia-production
|
||||
runtime_service: selfmedia-production
|
||||
runtime_service_port: "4317"
|
||||
health_path: /healthz
|
||||
health_url: http://selfmedia-production.selfmedia-production.svc.cluster.local:4317/healthz
|
||||
- id: pikaoa-nc01
|
||||
name: pikaoa-nc01
|
||||
namespace: pikaoa-ci
|
||||
@@ -447,15 +412,28 @@ consumers:
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
fsGroup: 1000
|
||||
- id: selfmedia-production-nc01
|
||||
repositoryRef: selfmedia-production-nc01
|
||||
repositoryRef: selfmedia-nc01
|
||||
node: NC01
|
||||
lane: selfmedia-production
|
||||
namespace: selfmedia-production-ci
|
||||
namespace: selfmedia-ci
|
||||
pipeline: selfmedia-production-nc01-pac
|
||||
pipelineRunPrefix: selfmedia-production-nc01
|
||||
argoNamespace: argocd
|
||||
argoApplication: selfmedia-production-nc01
|
||||
closeoutGitOpsMirrorFlush: false
|
||||
params:
|
||||
source_branch: release
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
||||
pipeline_name: selfmedia-production-nc01-pac
|
||||
pipeline_run_prefix: selfmedia-production-nc01
|
||||
service_account: selfmedia-production-nc01-tekton-runner
|
||||
image_repository: 127.0.0.1:5000/selfmedia/newsroom-production
|
||||
gitops_branch: nc01-selfmedia-production-gitops
|
||||
gitops_manifest_path: deploy/gitops/nc01-production/resources.yaml
|
||||
runtime_namespace: selfmedia-production
|
||||
runtime_deployment: selfmedia-production
|
||||
runtime_service: selfmedia-production
|
||||
health_url: http://selfmedia-production.selfmedia-production.svc.cluster.local:4317/healthz
|
||||
argoBootstrap:
|
||||
project: default
|
||||
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git
|
||||
|
||||
@@ -20,6 +20,7 @@ delivery:
|
||||
serviceAccountName: selfmedia-nc01-tekton-runner
|
||||
serviceAccountAutomount: false
|
||||
roleBindingName: selfmedia-nc01-tekton-runner
|
||||
parameterBinding: repository
|
||||
workspaceSize: 16Gi
|
||||
pipelineTimeout: 2h0m0s
|
||||
toolImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
@@ -243,11 +244,12 @@ delivery:
|
||||
node: NC01
|
||||
lane: selfmedia-production
|
||||
ci:
|
||||
namespace: selfmedia-production-ci
|
||||
namespace: selfmedia-ci
|
||||
pipeline: selfmedia-production-nc01-pac
|
||||
pipelineRunPrefix: selfmedia-production-nc01
|
||||
serviceAccountName: selfmedia-production-nc01-tekton-runner
|
||||
roleBindingName: selfmedia-production-nc01-tekton-runner
|
||||
parameterBinding: literal
|
||||
source:
|
||||
branch: release
|
||||
snapshotPrefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
||||
@@ -259,7 +261,7 @@ delivery:
|
||||
branch: nc01-selfmedia-production-gitops
|
||||
manifestPath: deploy/gitops/nc01-production/resources.yaml
|
||||
releaseStatePath: deploy/gitops-state/nc01-production/release.json
|
||||
credentialSecretName: pac-gitea-selfmedia-production-nc01
|
||||
credentialSecretName: pac-gitea-selfmedia-nc01
|
||||
author:
|
||||
name: SelfMedia NC01 Production CI
|
||||
email: selfmedia-production-nc01-ci@unidesk.local
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# R3.1 任务报告
|
||||
|
||||
已将 PaC 配置校验收敛为显式选中 consumer 的局部 fail-closed 与未选 consumer 的结构化非阻塞告警。YAML 解析、全局核心字段、选中 consumer 的 repository、namespace、安全、ownership 和有效参数错误继续阻塞;其他 consumer 的一致性错误输出 `consumer`、`configPath`、`code`、`blocking=false` 和脱敏 evidence。无效 consumer 不写入 marker 或 ServiceAccount 唯一性集合,聚合状态与历史只处理有效 consumer 并保留告警。
|
||||
|
||||
SELFMEDIA 开发与生产 consumer 共用仓库级 `selfmedia-nc01` Repository CR,production 通过 consumer 参数固定 release、独立 Pipeline、runner、GitOps 与 runtime。source artifact 已更新至 pikainc/selfmedia#29 的 `fba0019`,删除无语义空 `workspaces`。
|
||||
|
||||
验证:PaC/Gitea 定向测试 18 pass;source artifact 专项测试 31 pass;`check --syntax-only` 11/11;production source-artifact check aligned 且 worktree clean;production bootstrap dry-run `ok=true`、`mutation=false`、branch=release、repository=selfmedia-nc01/selfmedia-ci;`git diff --check` 通过。未执行 bootstrap confirm、人工 mirror、PipelineRun、Argo sync、合并或生产触发。
|
||||
@@ -23,3 +23,7 @@
|
||||
## R3 [in_progress]
|
||||
|
||||
依据 [UniDesk #2007](https://github.com/pikasTech/unidesk/issues/2007) 为 pikainc/selfmedia release 分支建立独立生产 PaC/GitOps 流水线:master 保持开发 consumer,release 精确驱动独立生产 namespace、Pipeline、GitOps branch/Application、runtime/PVC/Secret 与 YAML-first 公网端口;禁止 branch follower、手工同步和共享可写状态,以正常 release PR merge 验收生产公网入口、登录、digest 与 health,完成任务后将详细报告写入[任务报告](./details/selfmedia-production-delivery/R3_Task_Report.md)。
|
||||
|
||||
### R3.1 [completed]
|
||||
|
||||
落实 [UniDesk #2007](https://github.com/pikasTech/unidesk/issues/2007) 的局部配置校验降级:显式选中的 target/consumer 字段、安全与 ownership 错误继续 fail-closed;其他未选 consumer 的一致性错误输出含 consumer、config path、code、blocking=false 的结构化 warning,不阻塞开发 source-artifact/bootstrap/status 与聚合只读查询,并补定向测试,完成任务后将详细报告写入[任务报告](./details/selfmedia-production-delivery/R3.1_Task_Report.md)。
|
||||
|
||||
@@ -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 { runPlatformInfraPipelinesAsCodeCommand } from "./platform-infra-pipelines-as-code";
|
||||
import { readPacConfig, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig } from "./platform-infra-pipelines-as-code";
|
||||
|
||||
const mirror: GiteaMirrorRepository = {
|
||||
key: "widgets-nc01",
|
||||
@@ -53,6 +53,48 @@ test("PaC bootstrap uses one exact YAML-owned Gitea repository", () => {
|
||||
})).toThrow("2 exact matches");
|
||||
});
|
||||
|
||||
test("selfmedia consumers share one PaC Repository while keeping branch parameters isolated", () => {
|
||||
const config = Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
|
||||
const repositories = config.repositories as Array<Record<string, unknown>>;
|
||||
const consumers = config.consumers as Array<Record<string, unknown>>;
|
||||
const selfmediaRepositories = repositories.filter((item) => item.url === "https://gitea.pikapython.com/mirrors/pikainc-selfmedia");
|
||||
expect(selfmediaRepositories).toHaveLength(1);
|
||||
const development = consumers.find((item) => item.id === "selfmedia-nc01");
|
||||
const production = consumers.find((item) => item.id === "selfmedia-production-nc01");
|
||||
expect(development?.repositoryRef).toBe("selfmedia-nc01");
|
||||
expect(production?.repositoryRef).toBe("selfmedia-nc01");
|
||||
expect(development?.namespace).toBe("selfmedia-ci");
|
||||
expect(production?.namespace).toBe("selfmedia-ci");
|
||||
expect((selfmediaRepositories[0]?.params as Record<string, unknown>).source_branch).toBe("master");
|
||||
expect((production?.params as Record<string, unknown>).source_branch).toBe("release");
|
||||
expect((production?.params as Record<string, unknown>).pipeline_name).toBe("selfmedia-production-nc01-pac");
|
||||
});
|
||||
|
||||
test("unselected production validation is a non-blocking warning while selected production fails closed", () => {
|
||||
const config = structuredClone(readPacConfig({ consumerId: "selfmedia-nc01" }));
|
||||
const development = config.consumers.find((consumer) => consumer.id === "selfmedia-nc01");
|
||||
const production = config.consumers.find((consumer) => consumer.id === "selfmedia-production-nc01");
|
||||
expect(development?.deliveryProvenance).not.toBeNull();
|
||||
expect(production?.deliveryProvenance).not.toBeNull();
|
||||
if (development?.deliveryProvenance === null || development?.deliveryProvenance === undefined || production?.deliveryProvenance === null || production?.deliveryProvenance === undefined) {
|
||||
throw new Error("selfmedia delivery provenance fixtures are required");
|
||||
}
|
||||
production.params.gitops_secret_name = "invalid-production-secret";
|
||||
production.deliveryProvenance.markerValue = development.deliveryProvenance.markerValue;
|
||||
config.consumers = [production, development, ...config.consumers.filter((consumer) => consumer !== production && consumer !== development)];
|
||||
|
||||
const warnings = validatePacConfig(config, new Set(["selfmedia-nc01"]));
|
||||
const warning = warnings.find((item) => item.consumer === "selfmedia-production-nc01");
|
||||
expect(warning).toEqual({
|
||||
code: "pac-unselected-consumer-invalid",
|
||||
consumer: "selfmedia-production-nc01",
|
||||
configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.selfmedia-production-nc01",
|
||||
blocking: false,
|
||||
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
|
||||
});
|
||||
expect(() => validatePacConfig(config, new Set(["selfmedia-production-nc01"]))).toThrow();
|
||||
});
|
||||
|
||||
test("Gitea mirror bootstrap defaults to a visible dry-run instead of an empty refusal", () => {
|
||||
const rendered = renderMirrorBootstrap({
|
||||
ok: true,
|
||||
|
||||
@@ -105,6 +105,7 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
|
||||
{ id: "gitops-argo", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
|
||||
],
|
||||
repository: { id: repository.id, namespace: repository.namespace },
|
||||
warnings: result.warnings,
|
||||
blockers,
|
||||
next,
|
||||
valuesPrinted: false,
|
||||
@@ -119,6 +120,7 @@ export function renderPacBootstrap(result: Record<string, unknown>): RenderedCli
|
||||
const preconditions = records(projection.preconditions);
|
||||
const stages = records(projection.stages);
|
||||
const blockers = records(projection.blockers);
|
||||
const warnings = records(projection.warnings);
|
||||
const next = record(projection.next);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PAC / TEKTON / GITOPS BOOTSTRAP",
|
||||
@@ -133,6 +135,11 @@ export function renderPacBootstrap(result: Record<string, unknown>): RenderedCli
|
||||
"STAGES",
|
||||
...table(["STAGE", "OK", "STATE", "MUTATION"], stages.map((item) => [stringValue(item.id), boolText(item.ok), stringValue(item.state), boolText(item.mutation)])),
|
||||
...(blockers.length === 0 ? [] : ["", "BLOCKERS", ...blockers.map((item) => ` ${stringValue(item.code)}: ${compactLine(stringValue(item.reason))}`)]),
|
||||
...(warnings.length === 0 ? [] : [
|
||||
"",
|
||||
"NON-BLOCKING WARNINGS",
|
||||
...table(["CONSUMER", "BLOCKING", "CODE", "CONFIG"], warnings.map((item) => [stringValue(item.consumer), boolText(item.blocking), stringValue(item.code), stringValue(item.configPath)])),
|
||||
]),
|
||||
"",
|
||||
"NEXT",
|
||||
` ${stringValue(next.kind)}: ${stringValue(next.command)}`,
|
||||
|
||||
@@ -189,7 +189,7 @@ spec:
|
||||
key: $UNIDESK_PAC_WEBHOOK_SECRET_KEY
|
||||
concurrency_limit: $UNIDESK_PAC_CONCURRENCY_LIMIT
|
||||
params:
|
||||
$(printf '%s' "$UNIDESK_PAC_PARAMS_JSON" | node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(0,"utf8")); for (const [name,value] of Object.entries(p)) console.log(` - name: ${name}\n value: ${JSON.stringify(String(value))}`);')
|
||||
$(printf '%s' "$UNIDESK_PAC_REPOSITORY_PARAMS_JSON" | node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(0,"utf8")); for (const [name,value] of Object.entries(p)) console.log(` - name: ${name}\n value: ${JSON.stringify(String(value))}`);')
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +414,7 @@ export function renderPacSourceArtifactResult(result: Record<string, unknown>):
|
||||
const firstDrift = source.firstDrift === null ? null : record(source.firstDrift);
|
||||
const liveDrift = live?.firstDrift === null ? null : record(live?.firstDrift);
|
||||
const embeddedDrift = embedded?.firstDrift === null ? null : record(embedded?.firstDrift);
|
||||
const warnings = Array.isArray(result.warnings) ? result.warnings.map(record) : [];
|
||||
const lines = [
|
||||
"PAC SOURCE ARTIFACT",
|
||||
`target=${text(result.target)} consumer=${text(result.consumer)} node=${text(result.node)} lane=${text(result.lane)}`,
|
||||
@@ -427,6 +428,7 @@ export function renderPacSourceArtifactResult(result: Record<string, unknown>):
|
||||
`embedded=status:${text(embedded?.status)} name:${text(embedded?.name)} hash:${shortSha(embedded?.canonicalSha256)} provenance:${text(embedded?.provenanceAligned)} drift:${formatDrift(embeddedDrift)}`,
|
||||
`runtimeReason=live:${text(live?.reason)} embedded:${text(embedded?.reason)}`,
|
||||
`written=${Array.isArray(files.written) && files.written.length > 0 ? files.written.join(",") : "-"}`,
|
||||
...warnings.map((warning) => `warning=consumer:${text(warning.consumer)} blocking:${text(warning.blocking)} code:${text(warning.code)} config:${text(warning.configPath)}`),
|
||||
];
|
||||
if (typeof result.next === "string") lines.push(`next=${result.next}`);
|
||||
return renderedCliResult(result.ok !== false, String(result.action), `${lines.join("\n")}\n`);
|
||||
@@ -817,6 +819,9 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
|
||||
if (name === "source-stage-ref") return { name, value: "{{ source_snapshot_prefix }}/{{ revision }}" };
|
||||
if (name === "git-url") return { name, value: "{{ repo_url }}" };
|
||||
const repositoryParam = name.replaceAll("-", "_");
|
||||
if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" && Object.prototype.hasOwnProperty.call(param, "default")) {
|
||||
return { name, value: param.default };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
|
||||
return { name, value: `{{ ${repositoryParam} }}` };
|
||||
}
|
||||
@@ -826,6 +831,7 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
|
||||
}
|
||||
|
||||
function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
|
||||
if (desiredSpec.workspaces === undefined) return [];
|
||||
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
|
||||
const name = requiredString(workspace.name, "Pipeline.spec.workspaces[].name");
|
||||
if (name === "source") {
|
||||
|
||||
@@ -64,7 +64,7 @@ interface PacTarget {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface PacConfig {
|
||||
export interface PacConfig {
|
||||
version: number;
|
||||
kind: "platform-infra-pipelines-as-code";
|
||||
metadata: {
|
||||
@@ -139,6 +139,15 @@ interface PacConfig {
|
||||
};
|
||||
repositories: PacRepository[];
|
||||
consumers: PacConsumer[];
|
||||
validationWarnings: PacConfigValidationWarning[];
|
||||
}
|
||||
|
||||
export interface PacConfigValidationWarning {
|
||||
readonly code: "pac-unselected-consumer-invalid";
|
||||
readonly consumer: string;
|
||||
readonly configPath: string;
|
||||
readonly blocking: false;
|
||||
readonly evidence: string;
|
||||
}
|
||||
|
||||
interface PacRepository {
|
||||
@@ -185,6 +194,7 @@ interface PacConsumer {
|
||||
gitOps: { repoUrl: string; targetRevision: string };
|
||||
} | null;
|
||||
repositoryRef: string;
|
||||
params: Record<string, string>;
|
||||
closeoutGitOpsMirrorFlush: boolean;
|
||||
closeoutGitOpsMirrorLane: "v02" | "v03" | null;
|
||||
sourceArtifact: PacSourceArtifactSpec | null;
|
||||
@@ -294,7 +304,7 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
const structuredError = args.includes("--json") || args.includes("--full");
|
||||
try {
|
||||
const options = parsePacSourceArtifactOptions(args.slice(1));
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
|
||||
@@ -325,10 +335,13 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
cloneUrl: repository.cloneUrl,
|
||||
owner: repository.owner,
|
||||
repo: repository.repo,
|
||||
params: repository.params,
|
||||
params: consumerParams(repository, consumer),
|
||||
},
|
||||
};
|
||||
const result = await runPacSourceArtifact(binding, options, async ({ desiredSpec, desiredWrapper, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, desiredWrapper, provenance, sourceCommit));
|
||||
const result = {
|
||||
...await runPacSourceArtifact(binding, options, async ({ desiredSpec, desiredWrapper, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, desiredWrapper, provenance, sourceCommit)),
|
||||
warnings: pac.validationWarnings,
|
||||
};
|
||||
return options.json || options.full ? result : renderPacSourceArtifactResult(result);
|
||||
} catch (error) {
|
||||
const safeError = pacSourceArtifactSafeError(error);
|
||||
@@ -424,7 +437,7 @@ function help(scope: string | null): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function readPacConfig(): PacConfig {
|
||||
export function readPacConfig(selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean } = {}): PacConfig {
|
||||
const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code"), { label: configLabel }).value;
|
||||
const release = y.objectField(root, "release", "");
|
||||
const deliveryProvenance = y.objectField(root, "deliveryProvenance", "");
|
||||
@@ -527,11 +540,23 @@ function readPacConfig(): PacConfig {
|
||||
},
|
||||
repositories,
|
||||
consumers,
|
||||
validationWarnings: [],
|
||||
};
|
||||
validateConfig(parsed);
|
||||
const selectedConsumerIds = selectedPacConsumerIds(parsed, selection);
|
||||
parsed.validationWarnings = validateConfig(parsed, selectedConsumerIds);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function selectedPacConsumerIds(
|
||||
config: PacConfig,
|
||||
selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean },
|
||||
): ReadonlySet<string> {
|
||||
if (typeof selection.consumerId === "string" && selection.consumerId.length > 0) {
|
||||
return new Set([selection.consumerId.toLowerCase()]);
|
||||
}
|
||||
return selection.selectDefault === true ? new Set([config.defaults.consumerId.toLowerCase()]) : new Set();
|
||||
}
|
||||
|
||||
function parseRepository(repository: Record<string, unknown>, path: string): PacRepository {
|
||||
const name = y.kubernetesNameField(repository, "name", path);
|
||||
return {
|
||||
@@ -582,6 +607,7 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
|
||||
},
|
||||
deliveryProvenance,
|
||||
repositoryRef: typeof consumer.repositoryRef === "string" && consumer.repositoryRef.length > 0 ? consumer.repositoryRef : defaultRepositoryRef,
|
||||
params: consumer.params === undefined ? {} : stringRecord(y.objectField(consumer, "params", path), `${path}.params`),
|
||||
closeoutGitOpsMirrorFlush: y.booleanField(consumer, "closeoutGitOpsMirrorFlush", path),
|
||||
closeoutGitOpsMirrorLane: consumer.closeoutGitOpsMirrorLane === undefined ? null : y.enumField(consumer, "closeoutGitOpsMirrorLane", path, ["v02", "v03"] as const),
|
||||
sourceArtifact: consumer.sourceArtifact === undefined ? null : parseSourceArtifact(y.objectField(consumer, "sourceArtifact", path), `${path}.sourceArtifact`),
|
||||
@@ -819,7 +845,11 @@ function parseTarget(record: Record<string, unknown>, index: number): PacTarget
|
||||
};
|
||||
}
|
||||
|
||||
function validateConfig(config: PacConfig): void {
|
||||
export function validatePacConfig(config: PacConfig, selectedConsumerIds: ReadonlySet<string>): PacConfigValidationWarning[] {
|
||||
return validateConfig(config, selectedConsumerIds);
|
||||
}
|
||||
|
||||
function validateConfig(config: PacConfig, selectedConsumerIds: ReadonlySet<string>): PacConfigValidationWarning[] {
|
||||
if (config.version !== 1) throw new Error(`${configLabel}.version must be 1`);
|
||||
resolveTarget(config, config.defaults.targetId);
|
||||
if (config.release.waitTimeoutSeconds > 55) throw new Error(`${configLabel}.release.waitTimeoutSeconds must fit the 60s trans budget`);
|
||||
@@ -836,10 +866,40 @@ function validateConfig(config: PacConfig): void {
|
||||
const consumerIds = new Set<string>();
|
||||
const markerValues = new Set<string>();
|
||||
const reservedServiceAccounts = new Set<string>();
|
||||
const warnings: PacConfigValidationWarning[] = [];
|
||||
for (const consumer of config.consumers) {
|
||||
if (consumerIds.has(consumer.id)) throw new Error(`${configLabel}.consumers id must be unique: ${consumer.id}`);
|
||||
consumerIds.add(consumer.id);
|
||||
try {
|
||||
validateConsumerConfig(config, consumer, markerValues, reservedServiceAccounts);
|
||||
if (consumer.deliveryProvenance !== null) {
|
||||
markerValues.add(consumer.deliveryProvenance.markerValue);
|
||||
reservedServiceAccounts.add(`${consumer.node.toLowerCase()}\u0000${consumer.deliveryProvenance.executionServiceAccountName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedConsumerIds.has(consumer.id.toLowerCase())) throw error;
|
||||
warnings.push({
|
||||
code: "pac-unselected-consumer-invalid",
|
||||
consumer: consumer.id,
|
||||
configPath: `${configLabel}#consumers.${consumer.id}`,
|
||||
blocking: false,
|
||||
evidence: pacValueEvidence(error instanceof Error ? error.message : String(error)),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
|
||||
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function validateConsumerConfig(
|
||||
config: PacConfig,
|
||||
consumer: PacConsumer,
|
||||
markerValues: Set<string>,
|
||||
reservedServiceAccounts: Set<string>,
|
||||
): void {
|
||||
const repository = resolveRepository(config, consumer.repositoryRef);
|
||||
const params = consumerParams(repository, consumer);
|
||||
if (consumer.id.startsWith("sentinel-")) {
|
||||
if (repository.params.otel_traces_endpoint !== config.observability.tracesEndpoint) {
|
||||
throw new Error(`${configLabel}.repositories.${repository.id}.params.otel_traces_endpoint must match observability.tracesEndpoint`);
|
||||
@@ -855,7 +915,6 @@ function validateConfig(config: PacConfig): void {
|
||||
if (consumer.deliveryProvenance !== null) {
|
||||
if (consumer.sourceArtifact === null) throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance requires sourceArtifact ownership`);
|
||||
if (markerValues.has(consumer.deliveryProvenance.markerValue)) throw new Error(`${configLabel}.consumers deliveryProvenance.markerValue must be unique: ${consumer.deliveryProvenance.markerValue}`);
|
||||
markerValues.add(consumer.deliveryProvenance.markerValue);
|
||||
if (consumer.deliveryProvenance.executionServiceAccountName === "default") {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must not reserve the shared default ServiceAccount`);
|
||||
}
|
||||
@@ -863,9 +922,8 @@ function validateConfig(config: PacConfig): void {
|
||||
if (reservedServiceAccounts.has(serviceAccountKey)) {
|
||||
throw new Error(`${configLabel}.consumers deliveryProvenance.executionServiceAccountName must be unique per target: ${consumer.deliveryProvenance.executionServiceAccountName}`);
|
||||
}
|
||||
reservedServiceAccounts.add(serviceAccountKey);
|
||||
if (repository.params.service_account !== consumer.deliveryProvenance.executionServiceAccountName) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must match repository params.service_account`);
|
||||
if (params.service_account !== consumer.deliveryProvenance.executionServiceAccountName) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must match effective params.service_account`);
|
||||
}
|
||||
if (consumer.argoBootstrap !== null
|
||||
&& (consumer.argoBootstrap.repoUrl !== consumer.deliveryProvenance.gitOps.repoUrl
|
||||
@@ -885,13 +943,10 @@ function validateConfig(config: PacConfig): void {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private selfmedia repository`);
|
||||
}
|
||||
if (consumer.closeoutGitOpsMirrorFlush) throw new Error(`${configLabel}.consumers.${consumer.id}.closeoutGitOpsMirrorFlush must be false for Gitea writeback GitOps`);
|
||||
if (repository.params.gitops_secret_name !== repository.secretName) throw new Error(`${configLabel}.repositories.${repository.id}.params.gitops_secret_name must match secretName`);
|
||||
if (repository.params.gitops_username !== consumer.argoBootstrap.repositoryCredential.username) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo and Tekton Gitea usernames must match`);
|
||||
if (repository.params.gitops_read_url !== consumer.argoBootstrap.repoUrl) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo repoUrl must match params.gitops_read_url`);
|
||||
if (params.gitops_secret_name !== repository.secretName) throw new Error(`${configLabel}.consumers.${consumer.id}.effective params.gitops_secret_name must match repository secretName`);
|
||||
if (params.gitops_username !== consumer.argoBootstrap.repositoryCredential.username) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo and Tekton Gitea usernames must match`);
|
||||
if (params.gitops_read_url !== consumer.argoBootstrap.repoUrl) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo repoUrl must match effective params.gitops_read_url`);
|
||||
}
|
||||
}
|
||||
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
|
||||
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
|
||||
}
|
||||
|
||||
function numberInRange(value: unknown, minimum: number, maximum: number, path: string): number {
|
||||
@@ -933,8 +988,17 @@ function resolveRepository(config: PacConfig, repositoryRef: string): PacReposit
|
||||
return repository;
|
||||
}
|
||||
|
||||
function consumerParams(repository: PacRepository, consumer: PacConsumer): Record<string, string> {
|
||||
return { ...repository.params, ...consumer.params };
|
||||
}
|
||||
|
||||
function validPacConsumers(config: PacConfig): PacConsumer[] {
|
||||
const invalid = new Set(config.validationWarnings.map((warning) => warning.consumer.toLowerCase()));
|
||||
return config.consumers.filter((consumer) => !invalid.has(consumer.id.toLowerCase()));
|
||||
}
|
||||
|
||||
function plan(options: CommonOptions): Record<string, unknown> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
@@ -950,12 +1014,13 @@ function plan(options: CommonOptions): Record<string, unknown> {
|
||||
consumer: consumerObservationSummary(consumer),
|
||||
secrets,
|
||||
policy: policyChecks(repository),
|
||||
warnings: pac.validationWarnings,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
@@ -975,12 +1040,13 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
consumer: consumerObservationSummary(consumer),
|
||||
secrets: secretSummaries(pac),
|
||||
remote: parsed ?? compactCapture(result, { full: true }),
|
||||
warnings: pac.validationWarnings,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) {
|
||||
@@ -991,14 +1057,14 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
|
||||
try {
|
||||
mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, {
|
||||
...repository,
|
||||
sourceBranch: repository.params.source_branch,
|
||||
sourceBranch: consumerParams(repository, consumer).source_branch,
|
||||
});
|
||||
} catch (error) {
|
||||
return pacBootstrapYamlMatchFailure(target.id, consumer.id, error);
|
||||
return { ...pacBootstrapYamlMatchFailure(target.id, consumer.id, error), warnings: pac.validationWarnings };
|
||||
}
|
||||
const githubPreflight = record(await runGiteaMirrorBootstrapPreflight(config, target.id, mirror.key));
|
||||
if (githubPreflight.ok !== true) {
|
||||
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, { ok: false, mutation: false, mode: "skipped", githubPreflight, valuesPrinted: false }, { ok: false, mutation: false, mode: "skipped", valuesPrinted: false });
|
||||
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, { ok: false, mutation: false, mode: "skipped", githubPreflight, valuesPrinted: false }, { ok: false, mutation: false, mode: "skipped", valuesPrinted: false }, pac.validationWarnings);
|
||||
}
|
||||
const giteaArgs = ["mirror", "bootstrap", "--target", target.id, "--repo", mirror.key, ...(options.confirm ? ["--confirm"] : []), "--full"];
|
||||
const giteaBootstrap = record(await runPlatformInfraGiteaCommand(config, giteaArgs));
|
||||
@@ -1009,10 +1075,10 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
|
||||
mode: "skipped",
|
||||
error: "gitea-bootstrap-failed",
|
||||
valuesPrinted: false,
|
||||
});
|
||||
}, pac.validationWarnings);
|
||||
}
|
||||
const pacApply = await apply(config, options);
|
||||
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, pacApply);
|
||||
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, pacApply, pac.validationWarnings);
|
||||
}
|
||||
|
||||
function bootstrapResult(
|
||||
@@ -1024,6 +1090,7 @@ function bootstrapResult(
|
||||
githubPreflight: Record<string, unknown>,
|
||||
giteaBootstrap: Record<string, unknown>,
|
||||
pacApply: Record<string, unknown>,
|
||||
warnings: readonly PacConfigValidationWarning[],
|
||||
): Record<string, unknown> {
|
||||
const remote = record(pacApply.remote);
|
||||
const repositoryMissing = remote.error === "gitea-repository-missing";
|
||||
@@ -1053,6 +1120,7 @@ function bootstrapResult(
|
||||
],
|
||||
giteaBootstrap,
|
||||
pipelinesAsCodeApply: pacApply,
|
||||
warnings,
|
||||
next: options.confirm
|
||||
? {
|
||||
deliveryTrigger: `Merge the prepared GitHub PR for ${mirror.upstream.repository}@${mirror.upstream.branch}; the automatic chain owns mirror, PaC, Tekton, GitOps, Argo, and runtime convergence.`,
|
||||
@@ -1067,7 +1135,7 @@ function bootstrapResult(
|
||||
}
|
||||
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
@@ -1087,6 +1155,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
coverage: consumerCoverage(pac, target.id),
|
||||
summary,
|
||||
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw ? parsed : undefined,
|
||||
warnings: pac.validationWarnings,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
@@ -1095,7 +1164,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId ?? options.nodeId);
|
||||
const nodeId = options.nodeId ?? target.id;
|
||||
const consumers = pac.consumers.filter((consumer) => consumer.node.toLowerCase() === nodeId.toLowerCase());
|
||||
const consumers = validPacConsumers(pac).filter((consumer) => consumer.node.toLowerCase() === nodeId.toLowerCase());
|
||||
const startedAt = Date.now();
|
||||
const statuses = await Promise.all(consumers.map(async (consumer) => {
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
@@ -1139,6 +1208,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
||||
valuesPrinted: false,
|
||||
},
|
||||
consumers: statuses,
|
||||
warnings: pac.validationWarnings,
|
||||
summary: {
|
||||
ready: configured && ready,
|
||||
status: configured ? (ready ? "ready" : "blocked") : "warning",
|
||||
@@ -1157,7 +1227,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
||||
}
|
||||
|
||||
async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const startedAt = Date.now();
|
||||
@@ -1278,9 +1348,9 @@ function printCloseoutProgress(target: PacTarget, consumer: PacConsumer, stage:
|
||||
}
|
||||
|
||||
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const selection = resolvePacHistoryConsumerSelection(pac.consumers, target.id, options.consumerId, options.detailId);
|
||||
const selection = resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId);
|
||||
const selectedConsumers = selection.selectedConsumerIds.map((id) => resolveConsumer(pac, id));
|
||||
const firstConsumer = selectedConsumers[0];
|
||||
if (firstConsumer === undefined) throw new Error("no Pipelines-as-Code consumers are configured");
|
||||
@@ -1306,6 +1376,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
detailId: options.detailId,
|
||||
rows: arrayRecords(record(remote).rows),
|
||||
historyErrors,
|
||||
warnings: pac.validationWarnings,
|
||||
controlPlane: parsed === null ? undefined : {
|
||||
crdPresent: parsed.crdPresent === true,
|
||||
controllerReady: parsed.controllerReady,
|
||||
@@ -1361,9 +1432,9 @@ export function resolvePacHistoryConsumerSelection(
|
||||
}
|
||||
|
||||
async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const selection = resolvePacHistoryConsumerSelection(pac.consumers, target.id, options.consumerId, options.detailId);
|
||||
const selection = resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId);
|
||||
if (selection.selectedConsumerIds.length !== 1) {
|
||||
throw new Error("debug-step requires --consumer or --id resolving exactly one Pipelines-as-Code consumer");
|
||||
}
|
||||
@@ -1447,7 +1518,8 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
|
||||
UNIDESK_PAC_TOKEN_KEY: repository.tokenKey,
|
||||
UNIDESK_PAC_WEBHOOK_SECRET_KEY: repository.webhookSecretKey,
|
||||
UNIDESK_PAC_CONCURRENCY_LIMIT: String(repository.concurrencyLimit),
|
||||
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
|
||||
UNIDESK_PAC_REPOSITORY_PARAMS_JSON: JSON.stringify(repository.params),
|
||||
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(consumerParams(repository, consumer)),
|
||||
UNIDESK_PAC_REGISTRY_APPLICABILITY: registryApplicability(consumer, repository),
|
||||
UNIDESK_PAC_PIPELINE_NAME: consumer.pipeline,
|
||||
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
|
||||
@@ -1483,11 +1555,12 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
|
||||
}
|
||||
|
||||
function registryApplicability(consumer: PacConsumer, repository: PacRepository): "configured" | "not-configured" {
|
||||
const params = consumerParams(repository, consumer);
|
||||
const prefix = consumer.id === "unidesk-host" ? "unidesk_host_" : "";
|
||||
const imageRepository = repository.params[`${prefix}image_repository`] ?? repository.params.image_repository;
|
||||
const imageRepository = params[`${prefix}image_repository`] ?? params.image_repository;
|
||||
if (typeof imageRepository !== "string" || imageRepository.length === 0) return "not-configured";
|
||||
if (prefix.length > 0) return "configured";
|
||||
return repository.params.pipeline_name === consumer.pipeline ? "configured" : "not-configured";
|
||||
return params.pipeline_name === consumer.pipeline ? "configured" : "not-configured";
|
||||
}
|
||||
|
||||
function historyConsumerConfig(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
|
||||
@@ -1889,6 +1962,7 @@ function consumerObservationSummary(consumer: PacConsumer): Record<string, unkno
|
||||
argoNamespace: consumer.argoNamespace,
|
||||
argoApplication: consumer.argoApplication,
|
||||
repositoryRef: consumer.repositoryRef,
|
||||
params: Object.keys(consumer.params).sort(),
|
||||
runnerServiceAccount: consumer.runnerServiceAccount,
|
||||
argoRepositoryCredential: consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined ? null : {
|
||||
secretName: consumer.argoBootstrap.repositoryCredential.secretName,
|
||||
@@ -1899,11 +1973,11 @@ function consumerObservationSummary(consumer: PacConsumer): Record<string, unkno
|
||||
}
|
||||
|
||||
function consumerCoverage(pac: PacConfig, targetId: string): Record<string, unknown>[] {
|
||||
return pac.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
|
||||
return validPacConsumers(pac).filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
return {
|
||||
consumer: consumer.id,
|
||||
source: `${repository.owner}/${repository.repo}@${repository.params.source_branch ?? "-"}`,
|
||||
source: `${repository.owner}/${repository.repo}@${consumerParams(repository, consumer).source_branch ?? "-"}`,
|
||||
namespace: consumer.namespace,
|
||||
pipeline: consumer.pipeline,
|
||||
argoApplication: consumer.argoApplication,
|
||||
@@ -1945,11 +2019,13 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
|
||||
argoNamespace: consumer.argoNamespace,
|
||||
argoApplication: consumer.argoApplication,
|
||||
repositoryRef: consumer.repositoryRef,
|
||||
params: consumer.params,
|
||||
runnerServiceAccount: consumer.runnerServiceAccount,
|
||||
argoRepositoryCredential: consumer.argoRepositoryCredential,
|
||||
},
|
||||
secrets: result.secrets,
|
||||
policy: result.policy,
|
||||
warnings: result.warnings,
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -1972,6 +2048,7 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
|
||||
},
|
||||
deliveryAuthority: result.deliveryAuthority,
|
||||
capabilities: record(result.config).capabilities,
|
||||
warnings: result.warnings,
|
||||
summary: compactStatusSummary(record(result.summary)),
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
@@ -2176,6 +2253,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
||||
const internalPublish = record(record(config.capabilities).sentinelInternalPublish);
|
||||
const secrets = arrayRecords(result.secrets);
|
||||
const policy = arrayRecords(result.policy);
|
||||
const warnings = arrayRecords(result.warnings);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE PLAN",
|
||||
...table(["TARGET", "NAMESPACE", "RELEASE", "REPOSITORY", "CONSUMER"], [[stringValue(target.id), stringValue(repository.namespace), stringValue(record(config.release).version), stringValue(repository.name), stringValue(consumer.id)]]),
|
||||
@@ -2193,6 +2271,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
||||
"",
|
||||
"POLICY",
|
||||
...table(["NAME", "OK", "DETAIL"], policy.map((item) => [stringValue(item.name), boolText(item.ok), stringValue(item.detail)])),
|
||||
...renderPacConfigWarnings(warnings),
|
||||
"",
|
||||
"CAPABILITIES",
|
||||
...table(["NAME", "ENABLED", "PROVENANCE", "TRACKING"], [[
|
||||
@@ -2214,6 +2293,7 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
||||
const remote = record(result.remote);
|
||||
const preflight = record(remote.preflight);
|
||||
const errorText = compactLine(stringValue(remote.stderrTail, stringValue(remote.error, stringValue(result.error, ""))));
|
||||
const warnings = arrayRecords(result.warnings);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE APPLY",
|
||||
...table(["TARGET", "MODE", "MUTATION", "OK"], [[stringValue(target.id), stringValue(result.mode), boolText(result.mutation), boolText(result.ok)]]),
|
||||
@@ -2227,6 +2307,7 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
||||
]),
|
||||
...(result.ok === false && errorText.length > 0 ? ["", `ERROR ${errorText}`] : []),
|
||||
...(remote.error === "gitea-repository-missing" ? ["HELP bun scripts/cli.ts platform-infra pipelines-as-code help platform-bootstrap"] : []),
|
||||
...renderPacConfigWarnings(warnings),
|
||||
"",
|
||||
"NEXT",
|
||||
` status: ${stringValue(record(result.next).status)}`,
|
||||
@@ -2240,6 +2321,7 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
||||
const deliveryAuthority = record(result.deliveryAuthority);
|
||||
const internalPublish = record(record(record(result.config).capabilities).sentinelInternalPublish);
|
||||
const coverage = arrayRecords(result.coverage);
|
||||
const warnings = arrayRecords(result.warnings);
|
||||
const latest = record(summary.latestPipelineRun);
|
||||
const taskRuns = arrayRecords(summary.taskRuns);
|
||||
const artifact = record(summary.artifact);
|
||||
@@ -2258,6 +2340,7 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
||||
const webhooks = arrayRecords(summary.webhooks);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE STATUS",
|
||||
...renderPacConfigWarnings(warnings),
|
||||
...(coverage.length === 0 ? [] : [
|
||||
"COVERAGE",
|
||||
...table(["CONSUMER", "SOURCE", "NAMESPACE", "PIPELINE", "ARGO_APP"], coverage.map((item) => [stringValue(item.consumer), stringValue(item.source), stringValue(item.namespace), stringValue(item.pipeline), stringValue(item.argoApplication)])),
|
||||
@@ -2344,6 +2427,20 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
||||
return rendered(result, "platform-infra pipelines-as-code status", lines);
|
||||
}
|
||||
|
||||
function renderPacConfigWarnings(warnings: readonly Record<string, unknown>[]): string[] {
|
||||
if (warnings.length === 0) return [];
|
||||
return [
|
||||
"",
|
||||
"NON-BLOCKING WARNINGS",
|
||||
...table(["CONSUMER", "BLOCKING", "CODE", "CONFIG"], warnings.map((item) => [
|
||||
stringValue(item.consumer),
|
||||
boolText(item.blocking),
|
||||
stringValue(item.code),
|
||||
stringValue(item.configPath),
|
||||
])),
|
||||
];
|
||||
}
|
||||
|
||||
function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const consumer = record(result.consumer);
|
||||
@@ -2414,7 +2511,7 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
||||
}
|
||||
|
||||
async function deliveryTiming(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SelfMediaDeliveryTarget {
|
||||
readonly serviceAccountName: string;
|
||||
readonly serviceAccountAutomount: false;
|
||||
readonly roleBindingName: string;
|
||||
readonly parameterBinding: "repository" | "literal";
|
||||
readonly workspaceSize: string;
|
||||
readonly pipelineTimeout: string;
|
||||
readonly toolImage: string;
|
||||
@@ -193,6 +194,7 @@ function parseDeliveryTarget(id: string, value: Record<string, unknown>): SelfMe
|
||||
serviceAccountName: kubernetesName(ci.serviceAccountName, `${path}.ci.serviceAccountName`),
|
||||
serviceAccountAutomount: false,
|
||||
roleBindingName: kubernetesName(ci.roleBindingName, `${path}.ci.roleBindingName`),
|
||||
parameterBinding: enumValue(ci.parameterBinding, `${path}.ci.parameterBinding`, ["repository", "literal"] as const),
|
||||
workspaceSize: text(ci.workspaceSize, `${path}.ci.workspaceSize`),
|
||||
pipelineTimeout: text(ci.pipelineTimeout, `${path}.ci.pipelineTimeout`),
|
||||
toolImage: text(ci.toolImage, `${path}.ci.toolImage`),
|
||||
@@ -349,6 +351,12 @@ function boolean(value: unknown, path: string): boolean {
|
||||
return value;
|
||||
}
|
||||
|
||||
function enumValue<const T extends string>(value: unknown, path: string, allowed: readonly T[]): T {
|
||||
const result = text(value, path);
|
||||
if (!allowed.includes(result as T)) throw new Error(`${selfMediaConfigRef}.${path} must be one of ${allowed.join(", ")}`);
|
||||
return result as T;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown, path: string): string[] {
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0 || item.includes("\n"))) throw new Error(`${selfMediaConfigRef}.${path} must be an array of non-empty single-line strings`);
|
||||
return [...value] as string[];
|
||||
|
||||
@@ -75,7 +75,16 @@ function pipeline(target: SelfMediaDeliveryTarget, effectiveDeploymentB64: strin
|
||||
"gitops-username",
|
||||
"gitops-secret-name",
|
||||
];
|
||||
const taskParams = params.map((name) => ({ name, type: "string" }));
|
||||
const literalDefaults: Readonly<Record<string, string>> = {
|
||||
"source-snapshot-prefix": target.source.snapshotPrefix,
|
||||
"git-read-url": target.source.readUrl,
|
||||
"gitops-read-url": target.gitops.readUrl,
|
||||
"gitops-write-url": target.gitops.writeUrl,
|
||||
"gitops-username": target.gitops.credentialUsername,
|
||||
"gitops-secret-name": target.gitops.credentialSecretName,
|
||||
};
|
||||
const defaults = target.ci.parameterBinding === "literal" ? literalDefaults : {};
|
||||
const taskParams = params.map((name) => ({ name, type: "string", ...(defaults[name] === undefined ? {} : { default: defaults[name] }) }));
|
||||
const taskParamBindings = params.map((name) => ({ name, value: `$(params.${name})` }));
|
||||
return {
|
||||
apiVersion: "tekton.dev/v1",
|
||||
|
||||
Reference in New Issue
Block a user