diff --git a/config/platform-infra/gitea.yaml b/config/platform-infra/gitea.yaml index eb551567..1e72523e 100644 --- a/config/platform-infra/gitea.yaml +++ b/config/platform-infra/gitea.yaml @@ -400,6 +400,52 @@ sourceAuthority: naming: gitea-actions-immutable-source prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-master-nc01 legacyGitMirror: null + - key: selfmedia-production-nc01 + targetId: NC01 + credentialOverride: + github: + transport: https-token + sourceRef: pikainc-selfmedia-gh-token.txt + sourceKey: GH_TOKEN + format: raw-token + requiredFor: + - upstream-mirror + - mirror-sync + - managed-repository-fetch + - github-head-observe + - github-hooks-list + - github-hooks-reconcile + permissions: + contents: read + metadata: read + webhooks: read-write + gitFetchCredential: + apiVersion: unidesk.ai/v1 + kind: GitFetchCredential + authMode: github-https-token + host: github.com + secretRef: + namespace: devops-infra + name: gitea-github-sync-secrets + key: github-token-pikainc-selfmedia-release + upstream: + repository: pikainc/selfmedia + cloneUrl: https://github.com/pikainc/selfmedia.git + branch: release + visibility: private + gitea: + owner: mirrors + name: pikainc-selfmedia + mirrorMode: controlled-push + publicRead: false + readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git + gitops: + branch: nc01-selfmedia-production-gitops + flushDisposition: gitea-writeback + snapshot: + naming: gitea-actions-immutable-source + prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01 + legacyGitMirror: null - key: pikaoa-nc01 targetId: NC01 credentialOverride: diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index a7cc847e..2736f2d2 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -211,6 +211,41 @@ 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 @@ -411,6 +446,47 @@ consumers: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet fsGroup: 1000 + - id: selfmedia-production-nc01 + repositoryRef: selfmedia-production-nc01 + node: NC01 + lane: selfmedia-production + namespace: selfmedia-production-ci + pipeline: selfmedia-production-nc01-pac + pipelineRunPrefix: selfmedia-production-nc01 + argoNamespace: argocd + argoApplication: selfmedia-production-nc01 + closeoutGitOpsMirrorFlush: false + argoBootstrap: + project: default + repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git + targetRevision: nc01-selfmedia-production-gitops + path: deploy/gitops/nc01-production + destinationNamespace: selfmedia-production + automated: true + repositoryCredential: + secretName: argocd-repo-selfmedia-production-nc01 + username: unidesk-admin + deliveryProvenance: + required: true + markerValue: admission-pac-v2:selfmedia-production-nc01 + executionServiceAccountName: selfmedia-production-nc01-tekton-runner + gitOps: + repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git + targetRevision: nc01-selfmedia-production-gitops + runnerServiceAccount: + name: selfmedia-production-nc01-tekton-runner + automountServiceAccountToken: false + roleBindingName: selfmedia-production-nc01-tekton-runner + sourceArtifact: + mode: embedded-pipeline-spec + renderer: selfmedia-runtime + configRef: config/selfmedia.yaml#delivery.targets.NC01_PRODUCTION + pipelineRunPath: .tekton/selfmedia-production-nc01-pac.yaml + maxKeepRuns: 8 + taskRunTemplate: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + fsGroup: 1000 - id: pikaoa-nc01 repositoryRef: pikaoa-nc01 node: NC01 diff --git a/config/secrets-distribution.yaml b/config/secrets-distribution.yaml index 0accf54d..e041409e 100644 --- a/config/secrets-distribution.yaml +++ b/config/secrets-distribution.yaml @@ -219,6 +219,16 @@ targets: - selfmedia timeoutSeconds: 180 pollIntervalSeconds: 5 + - id: selfmedia-production-nc01 + route: NC01:k3s + namespace: selfmedia-production + scope: selfmedia-production + enabled: true + consumerRollout: + deployments: + - selfmedia-production + timeoutSeconds: 180 + pollIntervalSeconds: 5 - id: pikaoa-nc01 route: NC01:k3s namespace: pikaoa @@ -282,6 +292,42 @@ kubernetesSecrets: - sourceRef: ~/.codex/auth.json.pika sourceKey: contents targetKey: auth.json + - name: selfmedia-production-access + targetId: selfmedia-production-nc01 + secretName: selfmedia-production-access + type: Opaque + data: + - sourceRef: /root/.unidesk/.env/selfmedia-access.env + sourceKey: SELFMEDIA_ADMIN_USERNAME + targetKey: admin-username + - sourceRef: /root/.unidesk/.env/selfmedia-access.env + sourceKey: SELFMEDIA_ADMIN_PASSWORD + targetKey: admin-password + - sourceRef: /root/.unidesk/.env/selfmedia-access.env + sourceKey: SELFMEDIA_API_KEY + targetKey: api-key + - sourceRef: /root/.unidesk/.env/selfmedia-access.env + sourceKey: SELFMEDIA_SESSION_SECRET + targetKey: session-secret + - name: selfmedia-production-runtime + targetId: selfmedia-production-nc01 + secretName: selfmedia-production-runtime + type: Opaque + data: + - sourceRef: ~/.env/TOKEN + sourceKey: contents + targetKey: TOKEN + - name: selfmedia-production-codex + targetId: selfmedia-production-nc01 + secretName: selfmedia-production-codex + type: Opaque + data: + - sourceRef: ~/.codex/config.toml.pika + sourceKey: contents + targetKey: config.toml + - sourceRef: ~/.codex/auth.json.pika + sourceKey: contents + targetKey: auth.json - name: sub2rank-runtime targetId: sub2rank-nc01 secretName: sub2rank-secrets diff --git a/config/selfmedia.yaml b/config/selfmedia.yaml index ec4d9586..fc87be23 100644 --- a/config/selfmedia.yaml +++ b/config/selfmedia.yaml @@ -228,6 +228,62 @@ delivery: - --last sourcePath: /app skillsPath: /home/codex/.codex/skills + NC01_PRODUCTION: + extends: NC01 + node: NC01 + lane: selfmedia-production + ci: + namespace: selfmedia-production-ci + pipeline: selfmedia-production-nc01-pac + pipelineRunPrefix: selfmedia-production-nc01 + serviceAccountName: selfmedia-production-nc01-tekton-runner + roleBindingName: selfmedia-production-nc01-tekton-runner + source: + branch: release + snapshotPrefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01 + build: + imageRepository: 127.0.0.1:5000/selfmedia/newsroom-production + gitops: + 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 + author: + name: SelfMedia NC01 Production CI + email: selfmedia-production-nc01-ci@unidesk.local + deployment: + metadata: + name: selfmedia-production-nc01 + target: + id: NC01_PRODUCTION + namespace: selfmedia-production + runtime: + serviceAccountName: selfmedia-production + service: + name: selfmedia-production + publicExposure: + address: http://152.53.229.148:4319 + hostPort: 4319 + edge: + name: selfmedia-production-public-edge + upstream: selfmedia-production.selfmedia-production.svc.cluster.local:4317 + storage: + media: + claimName: selfmedia-production-data + codexSessions: + claimName: selfmedia-production-codex-sessions + secrets: + access: + target: + secretName: selfmedia-production-access + runtime: + target: + secretName: selfmedia-production-runtime + codex: + target: + secretName: selfmedia-production-codex + codex: + supervisorStatePath: /home/codex/.codex/state/selfmedia-production-supervisor.json cutover: targets: NC01: diff --git a/config/unidesk-cli.yaml b/config/unidesk-cli.yaml index 1a5ec1ea..cd1914de 100644 --- a/config/unidesk-cli.yaml +++ b/config/unidesk-cli.yaml @@ -27,12 +27,6 @@ github: sourceRef: pikainc-selfmedia-gh-token.txt sourceKey: GH_TOKEN format: raw-token - - repository: pikainc/pikaoa - priority: before-env - root: /root/.unidesk/.env - sourceRef: pikainc-selfmedia-gh-token.txt - sourceKey: GH_TOKEN - format: raw-token prMerge: unknownRetry: maxAttempts: 5 diff --git a/docs/MDTODO/details/selfmedia-production-delivery/R3_Task_Report.md b/docs/MDTODO/details/selfmedia-production-delivery/R3_Task_Report.md new file mode 100644 index 00000000..f9ef53e2 --- /dev/null +++ b/docs/MDTODO/details/selfmedia-production-delivery/R3_Task_Report.md @@ -0,0 +1,24 @@ +# R3 任务报告 + +## 当前交付检查点 + +- 已在 `config/selfmedia.yaml` 声明 `NC01_PRODUCTION` 生产 delivery target,并以显式 `extends` 复用开发基线、覆盖全部生产可写状态。 +- 已增加 `selfmedia-production-nc01` Repository/consumer,精确跟踪 `pikainc/selfmedia@release`,使用独立 CI namespace、Pipeline、snapshot、镜像仓、GitOps branch/Application 与运行 namespace。 +- 已声明独立 `selfmedia-production` Service、PVC、Secret、NetworkPolicy 和公网 `152.53.229.148:4319`;开发 `master` consumer 与 `4317` 入口保持不变。 +- bootstrap YAML 精确匹配加入 source branch,避免同仓库多分支 consumer 歧义。 +- 删除 `config/unidesk-cli.yaml` 中一项完全重复的 `pikainc/pikaoa` repository override,恢复受控 GitHub CLI fail-closed token 选择;未修改任何 token 内容。 +- 已通过 source-artifact renderer 在 `pikainc/selfmedia` release 专用分支生成 `.tekton/selfmedia-production-nc01-pac.yaml`,PR 为 https://github.com/pikainc/selfmedia/pull/29。 + +## 已完成验证 + +- `bun test scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts scripts/src/platform-infra-gitea-config.test.ts`:16 项通过。 +- `bun scripts/cli.ts check --syntax-only`:11 项通过。 +- `source-artifact check`:生产 PipelineSpec、provenance 与 owning YAML 对齐,source worktree clean。 +- `pipelines-as-code bootstrap --dry-run`:release 精确匹配,四阶段均为 planned,mutation=false。 +- `gh auth status --repo pikainc/selfmedia`:repository override、REST 与仓库访问正常,未披露凭据。 + +## 待主代理审核后的自动链验收 + +- 合并 UniDesk PR 后执行一次首发 `bootstrap --confirm`,只安装 YAML 声明的控制面,不同步源码、不创建人工 PipelineRun。 +- 审核并合并 `pikainc/selfmedia#29` 到 release,由正常 PR merge 自动驱动 webhook、Gitea snapshot、PaC、Tekton、GitOps/Argo 与 runtime。 +- 使用受控 status/source-artifact verify-runtime 验证生产 digest、health、独立公网端口与登录保护后完成 R3。 diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index da326b19..7e64dd45 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -26,19 +26,30 @@ test("PaC bootstrap uses one exact YAML-owned Gitea repository", () => { owner: "mirrors", repo: "acme-widgets", cloneUrl: `${mirror.gitea.readUrl}/`, + sourceBranch: "main", }); expect(selected.key).toBe("widgets-nc01"); + const releaseMirror = { ...mirror, key: "widgets-release-nc01", upstream: { ...mirror.upstream, branch: "release" } }; + expect(resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, releaseMirror]), "NC01", { + id: "widgets-release-nc01", + owner: "mirrors", + repo: "acme-widgets", + cloneUrl: mirror.gitea.readUrl, + sourceBranch: "release", + }).key).toBe("widgets-release-nc01"); expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([]), "NC01", { id: "widgets-nc01", owner: "mirrors", repo: "acme-widgets", cloneUrl: mirror.gitea.readUrl, + sourceBranch: "main", })).toThrow("no exact match"); expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, { ...mirror }]), "NC01", { id: "widgets-nc01", owner: "mirrors", repo: "acme-widgets", cloneUrl: mirror.gitea.readUrl, + sourceBranch: "main", })).toThrow("2 exact matches"); }); diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts index 86b3d309..550abbf0 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts @@ -6,6 +6,7 @@ export interface PacBootstrapRepositoryIdentity { readonly owner: string; readonly repo: string; readonly cloneUrl: string; + readonly sourceBranch: string; } export function resolvePacBootstrapMirrorRepository( @@ -18,12 +19,13 @@ export function resolvePacBootstrapMirrorRepository( && item.gitea.owner === repository.owner && item.gitea.name === repository.repo && repositoryUrlIdentity(item.gitea.readUrl) === repositoryUrlIdentity(repository.cloneUrl) + && item.upstream.branch === repository.sourceBranch )); if (candidates.length !== 1) { const disposition = candidates.length === 0 ? "no exact match" : `${candidates.length} exact matches`; throw new Error( `cannot resolve the YAML-owned Gitea bootstrap repository for PaC repository ${repository.id} on ${targetId}: ${disposition}; ` - + "match targetId, gitea owner/name, and read URL in config/platform-infra/gitea.yaml", + + "match targetId, gitea owner/name, read URL, and source branch in config/platform-infra/gitea.yaml", ); } return candidates[0]; diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 97ce662f..a71a29c4 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -989,7 +989,10 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise< const repository = resolveRepository(pac, consumer.repositoryRef); let mirror: ReturnType; try { - mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, repository); + mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, { + ...repository, + sourceBranch: repository.params.source_branch, + }); } catch (error) { return pacBootstrapYamlMatchFailure(target.id, consumer.id, error); } diff --git a/scripts/src/selfmedia-config.ts b/scripts/src/selfmedia-config.ts index 7cbb60e5..b9fc0f74 100644 --- a/scripts/src/selfmedia-config.ts +++ b/scripts/src/selfmedia-config.ts @@ -94,7 +94,8 @@ export function readSelfMediaConfig(): SelfMediaConfig { const cutover = record(root.cutover, "cutover"); const deliveryRecords = record(delivery.targets, "delivery.targets"); const cutoverRecords = record(cutover.targets, "cutover.targets"); - const deliveryTargets = Object.fromEntries(Object.entries(deliveryRecords).map(([id, value]) => [id, parseDeliveryTarget(id, record(value, `delivery.targets.${id}`))])); + const resolvedDeliveryRecords = resolveDeliveryRecords(deliveryRecords); + const deliveryTargets = Object.fromEntries(Object.entries(resolvedDeliveryRecords).map(([id, value]) => [id, parseDeliveryTarget(id, value)])); const cutoverTargets = Object.fromEntries(Object.entries(cutoverRecords).map(([id, value]) => [id, parseCutoverTarget(id, record(value, `cutover.targets.${id}`))])); const defaultTargetId = text(defaults.targetId, "defaults.targetId"); if (deliveryTargets[defaultTargetId] === undefined || cutoverTargets[defaultTargetId] === undefined) { @@ -111,6 +112,12 @@ export function resolveSelfMediaDeliveryTarget(targetId?: string | null): SelfMe return target; } +export function resolveSelfMediaDeliveryTargetByConfigRef(configRef: string): SelfMediaDeliveryTarget { + const prefix = `${selfMediaConfigRef}#delivery.targets.`; + if (!configRef.startsWith(prefix)) throw new Error(`selfmedia delivery configRef must start with ${prefix}`); + return resolveSelfMediaDeliveryTarget(configRef.slice(prefix.length)); +} + export function resolveSelfMediaCutoverTarget(targetId?: string | null): SelfMediaCutoverTarget { const config = readSelfMediaConfig(); const id = targetId ?? config.defaultTargetId; @@ -151,19 +158,19 @@ function parseDeliveryTarget(id: string, value: Record): SelfMe const publicExposure = record(deployment.publicExposure, `${path}.deployment.publicExposure`); const noProxy = stringArray(proxy.noProxy, `${path}.build.proxy.noProxy`); if (!noProxy.includes("hyueapi.com") || !noProxy.includes(".hyueapi.com")) throw new Error(`${path}.build.proxy.noProxy must preserve hyueapi.com and .hyueapi.com`); - if (text(value.node, `${path}.node`) !== id) throw new Error(`${path}.node must match target id ${id}`); + const node = text(value.node, `${path}.node`); if (text(target.id, `${path}.deployment.target.id`) !== id) throw new Error(`${path}.deployment.target.id must match ${id}`); if (text(source.repository, `${path}.source.repository`) !== "pikainc/selfmedia") throw new Error(`${path}.source.repository must be pikainc/selfmedia`); if (!text(source.snapshotPrefix, `${path}.source.snapshotPrefix`).startsWith("refs/unidesk/snapshots/")) throw new Error(`${path}.source.snapshotPrefix must be immutable`); if (boolean(ci.serviceAccountAutomount, `${path}.ci.serviceAccountAutomount`) !== false) throw new Error(`${path}.ci.serviceAccountAutomount must be false`); if (boolean(publicExposure.enabled, `${path}.deployment.publicExposure.enabled`) !== true) throw new Error(`${path}.deployment.publicExposure.enabled must be true`); - if (integer(publicExposure.hostPort, `${path}.deployment.publicExposure.hostPort`) !== 4317) throw new Error(`${path}.deployment.publicExposure.hostPort must be 4317`); + if (integer(publicExposure.hostPort, `${path}.deployment.publicExposure.hostPort`) <= 0) throw new Error(`${path}.deployment.publicExposure.hostPort must be positive`); validateAccessSecret(deployment, `${path}.deployment`); const gitopsWriteUrl = text(gitops.writeUrl, `${path}.gitops.writeUrl`); if (!gitopsWriteUrl.startsWith("http://gitea-http.")) throw new Error(`${path}.gitops.writeUrl must use internal Gitea authority`); return { id, - node: id, + node, lane: text(value.lane, `${path}.lane`), route: text(value.route, `${path}.route`), ci: { @@ -207,6 +214,38 @@ function parseDeliveryTarget(id: string, value: Record): SelfMe }; } +function resolveDeliveryRecords(records: Record): Record> { + const resolved: Record> = {}; + const resolving = new Set(); + const resolveOne = (id: string): Record => { + if (resolved[id] !== undefined) return resolved[id]; + if (resolving.has(id)) throw new Error(`${selfMediaConfigRef}.delivery.targets.${id}.extends contains a cycle`); + const raw = record(records[id], `delivery.targets.${id}`); + resolving.add(id); + const parentId = raw.extends === undefined ? null : text(raw.extends, `delivery.targets.${id}.extends`); + const own = Object.fromEntries(Object.entries(raw).filter(([key]) => key !== "extends")); + const effective = parentId === null ? structuredClone(own) : deepMerge(resolveOne(parentId), own); + resolving.delete(id); + resolved[id] = effective; + return effective; + }; + for (const id of Object.keys(records)) resolveOne(id); + return resolved; +} + +function deepMerge(base: Record, override: Record): Record { + const result = structuredClone(base); + for (const [key, value] of Object.entries(override)) { + const current = result[key]; + result[key] = isPlainRecord(current) && isPlainRecord(value) ? deepMerge(current, value) : structuredClone(value); + } + return result; +} + +function isPlainRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + function validateAccessSecret(deployment: Record, path: string): void { const secrets = record(deployment.secrets, `${path}.secrets`); const access = record(secrets.access, `${path}.secrets.access`); diff --git a/scripts/src/selfmedia-delivery-renderer.ts b/scripts/src/selfmedia-delivery-renderer.ts index acffbfe9..7722eff3 100644 --- a/scripts/src/selfmedia-delivery-renderer.ts +++ b/scripts/src/selfmedia-delivery-renderer.ts @@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process"; import { resolveSelfMediaDeliveryTarget, + resolveSelfMediaDeliveryTargetByConfigRef, selfMediaDeliveryConfigRef, selfMediaEffectiveDeployment, type SelfMediaDeliveryTarget, @@ -39,7 +40,7 @@ export interface SelfMediaRenderedPipeline { } export function renderSelfMediaDesiredPipeline(binding: SelfMediaRendererBinding, sourceWorktree: string): SelfMediaRenderedPipeline { - const target = resolveSelfMediaDeliveryTarget(binding.consumer.node); + const target = resolveSelfMediaDeliveryTargetByConfigRef(binding.consumer.sourceArtifact.configRef); const configRef = selfMediaDeliveryConfigRef(target.id); if (binding.consumer.sourceArtifact.renderer !== "selfmedia-runtime" || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") { throw new Error("selfmedia-runtime requires embedded-pipeline-spec"); @@ -320,7 +321,8 @@ function validateRuntimeRender(sourceWorktree: string, effectiveDeployment: Reco throw new Error(`selfmedia effective deployment failed service renderer validation: ${evidence}`); } const output = result.stdout; - const required = ["kind: Deployment", "kind: Service", "kind: PersistentVolumeClaim", "kind: NetworkPolicy", "hostPort: 4317", "automountServiceAccountToken: false"]; + const publicExposure = requiredRecord(effectiveDeployment.publicExposure, "deployment.publicExposure"); + const required = ["kind: Deployment", "kind: Service", "kind: PersistentVolumeClaim", "kind: NetworkPolicy", `hostPort: ${requiredInteger(publicExposure.hostPort, "deployment.publicExposure.hostPort")}`, "automountServiceAccountToken: false"]; for (const marker of required) { if (!output.includes(marker)) throw new Error(`selfmedia service renderer output is missing ${marker}`); } @@ -354,3 +356,13 @@ function requiredStringArray(value: unknown, path: string): string[] { if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be an array of non-empty strings`); return value as string[]; } + +function requiredRecord(value: unknown, path: string): Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`); + return value as Record; +} + +function requiredInteger(value: unknown, path: string): number { + if (!Number.isInteger(value)) throw new Error(`${path} must be an integer`); + return value as number; +}