diff --git a/config/pikaoa.yaml b/config/pikaoa.yaml index c4f4f167..b5eb3328 100644 --- a/config/pikaoa.yaml +++ b/config/pikaoa.yaml @@ -101,6 +101,15 @@ testRuntime: sourceKey: DATABASE_URL targetKey: DATABASE_URL runtime: + replicas: + api: 1 + worker: 1 + web: 1 + resources: + api: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } } + worker: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } } + web: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } } + initializer: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } } secretName: pikaoa-test-runtime apiServiceName: pikaoa-api apiPort: 8080 @@ -174,6 +183,163 @@ testRuntime: livenessPath: /healthz readinessPath: /healthz +releaseRuntime: + defaultTargetId: NC01 + targets: + NC01: + enabled: true + validationOnly: false + node: NC01 + route: NC01:k3s + namespace: pikaoa + ttlSeconds: 86400 + waitTimeoutSeconds: 300 + fieldManager: unidesk-pikaoa-release-runtime + taskState: + rootPath: /var/lib/unidesk/pikaoa-release-runtime + statusRequestTimeoutSeconds: 10 + logTailLines: 80 + cleanup: + mode: delete-namespace + requireOwnershipLabels: true + source: + mode: prebuilt-images + repository: pikainc/pikaoa + commitPolicy: cli-required + branch: release + worktreeRemote: https://github.com/pikainc/pikaoa.git + readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + snapshotPrefix: refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01 + images: + api: 127.0.0.1:5000/pikaoa/api:${commit} + worker: 127.0.0.1:5000/pikaoa/worker:${commit} + web: 127.0.0.1:5000/pikaoa/web:${commit} + initializer: 127.0.0.1:5000/pikaoa/api:${commit} + pullPolicy: IfNotPresent + ci: + lane: pikaoa + namespace: pikaoa-ci + pipeline: pikaoa-nc01-pac + pipelineRunPrefix: pikaoa-nc01 + serviceAccountName: pikaoa-nc01-tekton-runner + workspaceSize: 8Gi + pipelineTimeout: 20m0s + toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 + buildkitImage: 127.0.0.1:5000/hwlab/buildkit:rootless + build: + dockerfiles: + api: deploy/api.Dockerfile + worker: deploy/worker.Dockerfile + web: deploy/web.Dockerfile + images: + api: 127.0.0.1:5000/pikaoa/api + worker: 127.0.0.1:5000/pikaoa/worker + web: 127.0.0.1:5000/pikaoa/web + networkMode: host + gitops: + readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + writeUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + branch: nc01-pikaoa-gitops + manifestPath: deploy/gitops/nc01/resources.yaml + credential: + secretName: pac-gitea-pikaoa-nc01 + tokenKey: token + username: unidesk-admin + author: + name: PikaOA NC01 CI + email: pikaoa-nc01-ci@unidesk.local + database: + configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-database-url + name: pikaoa + username: pikaoa + schema: pikaoa + connection: + sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa.env + sourceKey: DATABASE_URL + targetKey: DATABASE_URL + runtime: + replicas: + api: 1 + worker: 1 + web: 1 + resources: + api: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } } + worker: { requests: { cpu: 100m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } } + web: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 500m, memory: 512Mi } } + initializer: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } } + secretName: pikaoa-runtime + apiServiceName: pikaoa-api + apiPort: 8080 + workerMetricsPort: 8081 + webPort: 8080 + webApiUpstreamEnv: PIKAOA_API_UPSTREAM + attachment: + storageRoot: /var/lib/pikaoa/attachments + maxBytes: 104857600 + claimName: pikaoa-attachments + storageRequest: 20Gi + storageClassName: null + fsGroup: 65532 + accessModes: + - ReadWriteOnce + workerInterval: 2s + outbox: + batchSize: 50 + maxAttempts: 4 + retryDelay: 2s + sessionTTL: 8h + shutdownTimeout: 10s + administrator: + username: admin + password: + sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt + targetKey: PIKAOA_BOOTSTRAP_ADMIN_PASSWORD + employee: + username: employee + password: + sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt + targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD + sessionSecret: + sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa.env + sourceKey: PIKAOA_SESSION_SECRET + targetKey: PIKAOA_SESSION_SECRET + initializer: + mode: fresh-database-only + jobName: pikaoa-init + backoffLimit: 2 + command: + - /usr/local/bin/pikaoa + - migrate + - up + - --config + - /etc/pikaoa/pikaoa.yaml + exposure: + serviceName: pikaoa-web + serviceType: NodePort + hostIP: 152.53.229.148 + port: 32096 + observability: + configRef: config/platform-infra/observability.yaml + otlpEndpoint: otel-collector.platform-infra.svc.cluster.local:4317 + prometheus: + configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector + scrape: true + apiPath: /metrics + workerPath: /metrics + exporterFailure: + warning: true + blocking: false + probes: + api: + livenessPath: /healthz + readinessPath: /readyz + worker: + livenessPath: /healthz + readinessPath: /readyz + web: + livenessPath: /healthz + readinessPath: /healthz + delivery: targets: NC01: diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 8436c436..442649ef 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -252,6 +252,45 @@ repositories: runtime_service_port: "8080" health_path: /healthz health_url: http://pikaoa-web.pikaoa-test.svc.cluster.local:8080/healthz + - id: pikaoa-nc01 + name: pikaoa-nc01 + namespace: pikaoa-ci + providerType: gitea + url: https://gitea.pikapython.com/mirrors/pikainc-pikaoa + cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + owner: mirrors + repo: pikainc-pikaoa + secretName: pac-gitea-pikaoa-nc01 + tokenKey: token + webhookSecretKey: webhook.secret + concurrencyLimit: 1 + params: + git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + source_branch: release + source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01 + node: NC01 + pipeline_name: pikaoa-nc01-pac + pipeline_run_prefix: pikaoa-nc01 + service_account: pikaoa-nc01-tekton-runner + pipeline_timeout: 20m0s + workspace_pvc_size: 8Gi + image_repository: 127.0.0.1:5000/pikaoa/api + api_image_repository: 127.0.0.1:5000/pikaoa/api + worker_image_repository: 127.0.0.1:5000/pikaoa/worker + web_image_repository: 127.0.0.1:5000/pikaoa/web + registry_probe_base: http://127.0.0.1:5000 + gitops_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + gitops_write_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + gitops_branch: nc01-pikaoa-gitops + gitops_username: unidesk-admin + gitops_secret_name: pac-gitea-pikaoa-nc01 + gitops_manifest_path: deploy/gitops/nc01/resources.yaml + runtime_namespace: pikaoa + runtime_deployment: pikaoa-api + runtime_service: pikaoa-web + runtime_service_port: "8080" + health_path: /healthz + health_url: http://pikaoa-web.pikaoa.svc.cluster.local:8080/healthz consumers: - extends: templates.consumers.agentrunV02 variables: @@ -509,7 +548,7 @@ consumers: dnsPolicy: ClusterFirstWithHostNet fsGroup: 1000 - id: pikaoa-nc01 - repositoryRef: pikaoa-test-nc01 + repositoryRef: pikaoa-nc01 node: NC01 lane: pikaoa namespace: pikaoa-ci @@ -541,10 +580,27 @@ consumers: repositoryCredential: secretName: argocd-repo-pikaoa-nc01 username: unidesk-admin + deliveryProvenance: + required: true + markerValue: admission-pac-v2:pikaoa-nc01 + executionServiceAccountName: pikaoa-nc01-tekton-runner + gitOps: + repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git + targetRevision: nc01-pikaoa-gitops runnerServiceAccount: name: pikaoa-nc01-tekton-runner automountServiceAccountToken: false roleBindingName: pikaoa-nc01-tekton-runner + sourceArtifact: + mode: embedded-pipeline-spec + renderer: pikaoa-release-runtime + configRef: config/pikaoa.yaml#releaseRuntime.targets.NC01 + pipelineRunPath: .tekton/pikaoa-nc01-pac.yaml + maxKeepRuns: 8 + taskRunTemplate: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + fsGroup: 1000 templates: repositories: agentrunV02: diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R1.6.1.3_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R1.6.1.3_Task_Report.md new file mode 100644 index 00000000..ccca5f78 --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R1.6.1.3_Task_Report.md @@ -0,0 +1,53 @@ +# R1.6.1.3 任务报告 + +## 任务回链 + +- MDTODO:`R1.6.1.3`。 +- GitHub:`pikasTech/unidesk#2129`、`pikasTech/unidesk#2110`。 +- Target:`NC01`。 +- 交付边界:本任务只完成生产 release 控制面、只读计划与定向测试,不执行生产 mutation。 + +## 已完成 + +- `config/pikaoa.yaml#releaseRuntime.targets.NC01` 成为生产运行面唯一 owning selector: + - namespace、单副本、resources、附件 PVC、Service、OTel、Prometheus、release source、fresh initializer policy 均由 YAML 声明; + - Secret 继续引用既有受控 distribution 与 PK01 PostgreSQL export,不打印值; + - 公网 hostname、FRP port、Caddy/TLS 事实保留在同一生产 delivery 声明。 +- `config/platform-infra/pipelines-as-code.yaml` 新增独立 `pikaoa-nc01` Repository/consumer: + - 只跟随 `release` 与 `refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01`; + - 与 `pikaoa-test-nc01/master` 的 Secret、PipelineRun、GitOps branch、Argo application 完全隔离; + - production sourceArtifact 使用 `pikaoa-release-runtime` renderer 生成 `.tekton/pikaoa-nc01-pac.yaml`。 +- test/release 共用 renderer 已修复真实 PaC 缺陷: + - TaskSpec workspace 声明 `mountPath: /workspace`,step 不再手工挂载 workspace,token Secret mounts 保留; + - BuildKit metadata 解析允许 JSON 冒号两侧空白,只接受 `containerimage.digest` 的完整 `sha256:<64hex>`; + - metadata 不可读时使用本地 registry manifest `Docker-Content-Digest` 受控兜底。 +- Gitea mirror webhook status 只解析选中 repository 的 credential 与 topology: + - `--repo pikaoa-nc01` 不再读取未选 `pikaoa-test-nc01` token; + - 现场状态显示 release commit `07a9a9db3cad` 已 committed。 + +## 验证证据 + +- `bun test scripts/src/pikaoa-test-delivery-renderer.test.ts scripts/src/platform-infra-gitea-config.test.ts scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts`:`24 pass / 0 fail`。 +- `platform-infra pipelines-as-code plan --target NC01 --consumer pikaoa-nc01`:识别独立 production Repository、consumer、Tekton 与 Argo 路径。 +- `platform-infra gitea mirror webhook status --target NC01 --repo pikaoa-nc01`:`READY=true`,release head、Gitea 与 snapshot 均为 `07a9a9db3cad`。 +- source-artifact `plan/check`:desired hash 已生成;产品 `.tekton/pikaoa-nc01-pac.yaml` 尚未写入,符合本任务禁止直接编辑固定产品主 worktree 的边界。 + +## 主代理后续命令 + +在 `/root/pikaoa` clean worktree 审核并写入产品 release artifact: + +```bash +bun scripts/cli.ts platform-infra pipelines-as-code source-artifact plan --target NC01 --consumer pikaoa-nc01 --source-worktree /root/pikaoa +bun scripts/cli.ts platform-infra pipelines-as-code source-artifact write --target NC01 --consumer pikaoa-nc01 --source-worktree /root/pikaoa --confirm +bun scripts/cli.ts platform-infra pipelines-as-code source-artifact check --target NC01 --consumer pikaoa-nc01 --source-worktree /root/pikaoa +``` + +产品 artifact 提交、正常 release PR merge 后,由主代理执行只读 bootstrap 计划并按正常入口触发交付: + +```bash +bun scripts/cli.ts platform-infra gitea mirror webhook status --target NC01 --repo pikaoa-nc01 +bun scripts/cli.ts platform-infra pipelines-as-code plan --target NC01 --consumer pikaoa-nc01 +bun scripts/cli.ts platform-infra pipelines-as-code status --target NC01 --consumer pikaoa-nc01 +``` + +交付完成后再按原入口验收 `pikaoa` namespace、initializer、API/Worker/Web、附件 PVC、OTel/Prometheus 与 `https://oa.pikapython.com`;不得人工创建 PipelineRun、Argo refresh 或生产 namespace mutation。 diff --git a/docs/MDTODO/pikaoa-enterprise-platform.md b/docs/MDTODO/pikaoa-enterprise-platform.md index 50c3991d..02b6cb97 100644 --- a/docs/MDTODO/pikaoa-enterprise-platform.md +++ b/docs/MDTODO/pikaoa-enterprise-platform.md @@ -145,6 +145,12 @@ ##### R1.6.1.1 [completed] 将 PK01 PostgreSQL 逻辑备份正规化为 YAML-first 多数据库 logicalDumps,保留 Sub2API 并新增 PikaOA 独立 dump、timer、retention 与受控状态/可恢复性摘要(pikasTech/unidesk#2115),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.6.1.1_Task_Report.md)。 +##### R1.6.1.2 + +为 NC01 pikaoa namespace 的生产附件 PVC 建立 YAML-first Restic 离机备份、受控状态与 restore smoke,复用 PK01 /var/backups/unidesk 且失败只告警不阻塞业务,执行记录见 [pikasTech/unidesk#2128](https://github.com/pikasTech/unidesk/issues/2128),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.6.1.2_Task_Report.md)。 +##### R1.6.1.3 [completed] + +建立仅跟随产品 release 的 PikaOA 生产 PaC/Tekton/GitOps/Argo lane,以 fresh initializer 在 NC01 pikaoa namespace 启动 API/Worker/Web/附件 PVC,接入 PK01 host PostgreSQL、OTel/Prometheus 和 oa.pikapython.com TLS 公网入口,执行记录见 [pikasTech/unidesk#2129](https://github.com/pikasTech/unidesk/issues/2129),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.6.1.3_Task_Report.md)。 ### R1.7 通过 CLI、OTel、Prometheus 和 https://oa.pikapython.com 桌面/移动原入口验证完整主路径,依赖 R1.6,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.7_Task_Report.md)。 diff --git a/scripts/src/pikaoa-test-delivery-renderer.test.ts b/scripts/src/pikaoa-test-delivery-renderer.test.ts index d0bc0503..003b2eb2 100644 --- a/scripts/src/pikaoa-test-delivery-renderer.test.ts +++ b/scripts/src/pikaoa-test-delivery-renderer.test.ts @@ -9,13 +9,13 @@ function pacDocument(): Record { return Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record; } -function rendererBinding(): Parameters[0] { +function rendererBinding(id = "pikaoa-test-nc01"): Parameters[0] { const document = pacDocument(); const repositories = document.repositories as Array>; const consumers = document.consumers as Array>; - const repository = repositories.find((item) => item.id === "pikaoa-test-nc01"); - const consumer = consumers.find((item) => item.id === "pikaoa-test-nc01"); - if (repository === undefined || consumer === undefined) throw new Error("PikaOA test PaC fixture is required"); + const consumer = consumers.find((item) => item.id === id); + const repository = repositories.find((item) => item.id === consumer?.repositoryRef); + if (repository === undefined || consumer === undefined) throw new Error(`PikaOA PaC fixture is required: ${id}`); return { consumer, repository } as Parameters[0]; } @@ -34,12 +34,31 @@ describe("PikaOA test PaC renderer", () => { const repository = repositories.find((item) => item.id === "pikaoa-test-nc01"); const testConsumer = consumers.find((item) => item.id === "pikaoa-test-nc01"); const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01"); + const productionRepository = repositories.find((item) => item.id === "pikaoa-nc01"); expect((repository?.params as Record).source_branch).toBe("master"); expect((testConsumer?.sourceArtifact as Record).pipelineRunPath).toBe(".tekton/pikaoa-test-nc01-pac.yaml"); expect((testConsumer?.sourceArtifact as Record).renderer).toBe("pikaoa-test-runtime"); expect((productionConsumer?.params as Record).source_branch).toBe("release"); expect((productionConsumer?.params as Record).source_snapshot_prefix).toBe("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01"); - expect(productionConsumer?.sourceArtifact).toBeUndefined(); + expect(productionConsumer?.repositoryRef).toBe("pikaoa-nc01"); + expect((productionRepository?.params as Record).source_branch).toBe("release"); + expect((productionConsumer?.sourceArtifact as Record).renderer).toBe("pikaoa-release-runtime"); + expect((productionConsumer?.sourceArtifact as Record).configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01"); + }); + + test("renders release runtime from its isolated owning selector", () => { + const source = sourceFixture(); + try { + const rendered = renderPikaoaTestDesiredPipeline(rendererBinding("pikaoa-nc01"), source); + expect(rendered.configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01"); + const serialized = JSON.stringify(rendered.pipeline); + expect(serialized).toContain("pikaoa-nc01-pac"); + expect(serialized).toContain("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01"); + expect(serialized).toContain("pac-gitea-pikaoa-nc01"); + expect(serialized).not.toContain("pikaoa-test-nc01-pac"); + } finally { + rmSync(source, { recursive: true, force: true }); + } }); test("renders parallel digest builds and a PreSync fresh database initializer", () => { @@ -65,15 +84,30 @@ describe("PikaOA test PaC renderer", () => { expect(JSON.stringify(task)).not.toContain("source-token"); const buildScript = (((task?.taskSpec as Record).steps as Array>)[0]?.script) as string; expect(buildScript).toContain('"containerimage.digest"[[:space:]]*:[[:space:]]*"\\(sha256:[0-9a-f]\\{64\\}\\)"'); + expect(buildScript).toContain(".*/\\1/p"); + expect(buildScript).not.toContain("\u0001"); expect(buildScript).toContain("Docker-Content-Digest:"); expect(buildScript).toContain("build_status=$?"); expect(buildScript).toContain('"code":"buildctl-nonzero-artifact-verified"'); expect(buildScript).toContain('"artifactVerified":true'); } + const digest = "sha256:" + "a".repeat(64); + const metadata = JSON.stringify({ "containerimage.digest": digest }, null, 2); + const parsedDigest = /"containerimage\.digest"\s*:\s*"(sha256:[0-9a-f]{64})"/u.exec(metadata)?.[1]; + expect(parsedDigest).toBe(digest); const sourceTask = tasks.find((item) => item.name === "source"); expect(JSON.stringify(sourceTask)).toContain("source-token"); expect(JSON.stringify(sourceTask)).toContain("GIT_ASKPASS"); expect(JSON.stringify(sourceTask)).toContain("$(params.gitops-secret-name)"); + for (const task of tasks) { + const taskSpec = task.taskSpec as Record | undefined; + if (taskSpec === undefined) continue; + expect(taskSpec.workspaces).toEqual([{ name: "workspace", mountPath: "/workspace" }]); + const steps = taskSpec.steps as Array>; + for (const step of steps) { + expect((step.volumeMounts as Array> | undefined)?.some((mount) => mount.name === "workspace") ?? false).toBe(false); + } + } const publish = tasks.find((item) => item.name === "gitops-publish"); expect(publish?.runAfter).toEqual(["build-api", "build-worker", "build-web"]); const script = ((((publish?.taskSpec as Record).steps as Array>)[0]?.script) as string); diff --git a/scripts/src/pikaoa-test-delivery-renderer.ts b/scripts/src/pikaoa-test-delivery-renderer.ts index 94af7c24..b3d760f7 100644 --- a/scripts/src/pikaoa-test-delivery-renderer.ts +++ b/scripts/src/pikaoa-test-delivery-renderer.ts @@ -3,6 +3,8 @@ import { resolve } from "node:path"; import { pikaoaTestRuntimeConfigRef, + pikaoaReleaseRuntimeConfigRef, + resolvePikaoaReleaseRuntimeTargetByConfigRef, resolvePikaoaTestRuntimeTargetByConfigRef, type TestTargetSpec, } from "./pikaoa-test-target"; @@ -37,10 +39,13 @@ export interface PikaoaTestRenderedPipeline { } export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBinding, sourceWorktree: string): PikaoaTestRenderedPipeline { - const target = resolvePikaoaTestRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef); - const configRef = pikaoaTestRuntimeConfigRef(target.id); - if (binding.consumer.sourceArtifact.renderer !== "pikaoa-test-runtime" || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") { - throw new Error("pikaoa-test-runtime requires embedded-pipeline-spec"); + const release = binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"; + const target = release + ? resolvePikaoaReleaseRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef) + : resolvePikaoaTestRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef); + const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaTestRuntimeConfigRef(target.id); + if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-test-runtime") || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") { + throw new Error(`${binding.consumer.sourceArtifact.renderer} requires embedded-pipeline-spec`); } if (binding.consumer.sourceArtifact.configRef !== configRef) { throw new Error(`sourceArtifact.configRef must equal resolved owning selector ${configRef}`); @@ -59,6 +64,10 @@ export function pikaoaTestSourceWorktreeRemote(targetId: string): string { return resolvePikaoaTestRuntimeTargetByConfigRef(pikaoaTestRuntimeConfigRef(targetId)).source.worktreeRemote; } +export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string { + return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote; +} + function pipeline(target: TestTargetSpec): Record { const runtimeManifest = renderGitOpsManifest(target); return { @@ -280,7 +289,7 @@ git -C /workspace/gitops add -- ${shellSingleQuote(target.gitops.manifestPath)} if git -C /workspace/gitops diff --cached --quiet; then changed=false else - git -C /workspace/gitops commit -m "chore(gitops): deploy pikaoa test $SOURCE_COMMIT" + git -C /workspace/gitops commit -m "chore(gitops): deploy pikaoa $SOURCE_COMMIT" git -C /workspace/gitops push "$(params.gitops-write-url)" HEAD:${shellSingleQuote(target.gitops.branch)} changed=true fi @@ -329,7 +338,7 @@ function renderGitOpsManifest(target: TestTargetSpec): string { function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBinding): void { const expected: Readonly> = { - id: "pikaoa-test-nc01", + id: target.source.branch === "release" ? "pikaoa-nc01" : "pikaoa-test-nc01", node: target.node, lane: target.ci.lane, namespace: target.ci.namespace, @@ -351,7 +360,7 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin serviceAccount: binding.repository.params.service_account, }; for (const [key, value] of Object.entries(expected)) { - if (actual[key] !== value) throw new Error(`pikaoa-test-runtime binding ${key} must equal owning YAML value ${value}`); + if (actual[key] !== value) throw new Error(`PikaOA runtime binding ${key} must equal owning YAML value ${value}`); } } diff --git a/scripts/src/pikaoa-test-runtime-manifest.ts b/scripts/src/pikaoa-test-runtime-manifest.ts index a688348e..2a2e8567 100644 --- a/scripts/src/pikaoa-test-runtime-manifest.ts +++ b/scripts/src/pikaoa-test-runtime-manifest.ts @@ -27,8 +27,6 @@ export interface PikaoaTestManifestOptions { initializerHook?: "argo-presync"; } -const MANAGED_BY = "unidesk-pikaoa-test-target"; -const TEST_RUNTIME_LABEL = "pikaoa.unidesk.io/test-runtime"; const TARGET_LABEL = "pikaoa.unidesk.io/target"; const INSTANCE_LABEL = "pikaoa.unidesk.io/instance"; @@ -97,6 +95,7 @@ export function renderPikaoaTestRuntimeManifest( containers: [{ name: "initializer", image: context.initializerImage, imagePullPolicy: target.source.pullPolicy, command: target.initializer.command, + resources: target.runtime.resources.initializer, env: [{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" }], volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }], }], @@ -136,11 +135,12 @@ export function pikaoaManifestForStep(manifest: Record[], step: } export function ownershipLabels(target: TestTargetSpec, context: PikaoaTestRenderContext): Record { + const release = target.source.branch === "release"; return { "app.kubernetes.io/name": "pikaoa", - "app.kubernetes.io/part-of": "pikaoa-test-runtime", - "app.kubernetes.io/managed-by": MANAGED_BY, - [TEST_RUNTIME_LABEL]: "true", + "app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-test-runtime", + "app.kubernetes.io/managed-by": target.fieldManager, + [release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/test-runtime"]: "true", [TARGET_LABEL]: target.id, [INSTANCE_LABEL]: context.instanceId, }; @@ -191,13 +191,13 @@ function deployment( return { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: `pikaoa-${component}`, namespace: context.namespace, labels: { ...labels, ...selector } }, spec: { - replicas: 1, selector: { matchLabels: selector }, + replicas: target.runtime.replicas[component], selector: { matchLabels: selector }, template: { metadata: { labels: { ...labels, ...selector, ...podLabels }, annotations }, spec: { ...(component === "api" ? { securityContext: { fsGroup: target.runtime.attachment.fsGroup } } : {}), containers: [{ - name: component, image, imagePullPolicy: target.source.pullPolicy, env, + name: component, image, imagePullPolicy: target.source.pullPolicy, env, resources: target.runtime.resources[component], ports: [{ name: component === "worker" ? "metrics" : "http", containerPort: port }], volumeMounts: [ ...(component === "web" ? [] : [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }]), @@ -241,7 +241,7 @@ function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderCont ` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`, ` maxBytes: ${target.runtime.attachment.maxBytes}`, "observability:", - ` serviceName: pikaoa-test-${context.instanceId}`, + ` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-test-${context.instanceId}`}`, ` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`, `shutdownTimeout: ${target.runtime.shutdownTimeout}`, "", diff --git a/scripts/src/pikaoa-test-target.ts b/scripts/src/pikaoa-test-target.ts index 89fe6184..b99f5b8c 100644 --- a/scripts/src/pikaoa-test-target.ts +++ b/scripts/src/pikaoa-test-target.ts @@ -118,6 +118,8 @@ export interface TestTargetSpec { connection: SecretSourceSpec; }; runtime: { + replicas: { api: number; worker: number; web: number }; + resources: Record<"api" | "worker" | "web" | "initializer", Record>; secretName: string; apiServiceName: string; apiPort: number; @@ -318,8 +320,8 @@ function readSelection(options: TestTargetOptions): Selection { }; } -function parseTarget(id: string, root: Record, configLabel: string): TestTargetSpec { - const path = `${configLabel}.testRuntime.targets.${id}`; +function parseTarget(id: string, root: Record, configLabel: string, runtimeSection = "testRuntime"): TestTargetSpec { + const path = `${configLabel}.${runtimeSection}.targets.${id}`; simpleId(id, "target id"); const source = record(root.source, `${path}.source`); const images = record(source.images, `${path}.source.images`); @@ -332,6 +334,8 @@ function parseTarget(id: string, root: Record, configLabel: str const gitopsAuthor = record(gitops.author, `${path}.gitops.author`); const database = record(root.database, `${path}.database`); const runtime = record(root.runtime, `${path}.runtime`); + const replicas = record(runtime.replicas, `${path}.runtime.replicas`); + const resources = record(runtime.resources, `${path}.runtime.resources`); const attachment = record(runtime.attachment, `${path}.runtime.attachment`); const outbox = record(runtime.outbox, `${path}.runtime.outbox`); const administrator = record(runtime.administrator, `${path}.runtime.administrator`); @@ -426,6 +430,17 @@ function parseTarget(id: string, root: Record, configLabel: str connection: secretSource(database.connection, "database.connection", `${path}.database.connection`, true), }, runtime: { + replicas: { + api: positiveInteger(replicas.api, `${path}.runtime.replicas.api`, 20), + worker: positiveInteger(replicas.worker, `${path}.runtime.replicas.worker`, 20), + web: positiveInteger(replicas.web, `${path}.runtime.replicas.web`, 20), + }, + resources: { + api: record(resources.api, `${path}.runtime.resources.api`), + worker: record(resources.worker, `${path}.runtime.resources.worker`), + web: record(resources.web, `${path}.runtime.resources.web`), + initializer: record(resources.initializer, `${path}.runtime.resources.initializer`), + }, secretName: kubernetesName(nonEmpty(runtime.secretName, `${path}.runtime.secretName`), `${path}.runtime.secretName`, 63), apiServiceName: kubernetesName(nonEmpty(runtime.apiServiceName, `${path}.runtime.apiServiceName`), `${path}.runtime.apiServiceName`, 63), apiPort: positiveInteger(runtime.apiPort, `${path}.runtime.apiPort`, 65_535), @@ -505,7 +520,21 @@ export function resolvePikaoaTestRuntimeTargetByConfigRef(configRef: string): Te if (resolved.file !== "config/pikaoa.yaml" || match === null) { throw new Error("PikaOA test runtime configRef must select config/pikaoa.yaml#testRuntime.targets."); } - return parseTarget(match[1]!, record(resolved.value, configRef), resolved.file); + return parseTarget(match[1]!, record(resolved.value, configRef), resolved.file, "testRuntime"); +} + +export function pikaoaReleaseRuntimeConfigRef(targetId: string): string { + simpleId(targetId, "PikaOA release runtime target id"); + return `config/pikaoa.yaml#releaseRuntime.targets.${targetId}`; +} + +export function resolvePikaoaReleaseRuntimeTargetByConfigRef(configRef: string): TestTargetSpec { + const resolved = resolveConfigRef(configRef, "PikaOA release runtime configRef"); + const match = /^releaseRuntime\.targets\.([A-Za-z0-9._-]+)$/u.exec(resolved.fragment); + if (resolved.file !== "config/pikaoa.yaml" || match === null) { + throw new Error("PikaOA release runtime configRef must select config/pikaoa.yaml#releaseRuntime.targets."); + } + return parseTarget(match[1]!, record(resolved.value, configRef), resolved.file, "releaseRuntime"); } function planPayload(options: TestTargetOptions, selection: Selection): Record { diff --git a/scripts/src/platform-infra-gitea-config.test.ts b/scripts/src/platform-infra-gitea-config.test.ts index bce72fc8..45c9fe65 100644 --- a/scripts/src/platform-infra-gitea-config.test.ts +++ b/scripts/src/platform-infra-gitea-config.test.ts @@ -103,6 +103,25 @@ describe("platform-infra Gitea repository GitHub credentials", () => { expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.github.sourceRef); }); + test("production PikaOA status scope never reads the unselected master credential", () => { + const config = readGiteaConfig(); + const production = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-nc01"); + expect(production).toBeDefined(); + const requested: string[] = []; + const result = buildGiteaMirrorBootstrapCredentialPreflight(config, [production!], (credential) => { + requested.push(credential.gitFetchCredential.secretRef.key); + return { [credential.sourceKey]: "fixture-release-token" }; + }); + + expect(result.blockers).toEqual([]); + expect(requested).toEqual(["github-token-pikainc-pikaoa-release"]); + expect(result.credentials).toHaveLength(1); + expect(result.credentials[0]).toMatchObject({ + id: "github-upstream:pikaoa-nc01", + valuesPrinted: false, + }); + }); + test("does not inject the NC01 selfmedia token into JD01 bridge containers", () => { const config = readGiteaConfig(); const selfmediaTokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia"); diff --git a/scripts/src/platform-infra-gitea-remote.sh b/scripts/src/platform-infra-gitea-remote.sh index f91c62f1..340c5450 100644 --- a/scripts/src/platform-infra-gitea-remote.sh +++ b/scripts/src/platform-infra-gitea-remote.sh @@ -1074,7 +1074,9 @@ rows = [] for repo in repos: repository = repo["upstream"]["repository"] topology_spec = next((item for item in topology_repositories if isinstance(item, dict) and item.get("repository") == repository), {}) - token = os.environ[topology_spec.get("githubTokenEnv") or os.environ["UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV"]] + credential_spec = repo.get("githubCredential") or {} + token_env = credential_spec.get("tokenEnv") or topology_spec.get("githubTokenEnv") or os.environ["UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV"] + token = os.environ[token_env] branch = repo["upstream"]["branch"] result = request(f"/repos/{repository}/hooks") hooks_result = parse_github_hooks_response(result.get("ok"), result.get("status"), result.get("body")) diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index 2e85410c..0a7ffe78 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -1182,6 +1182,9 @@ function envVars(gitea: GiteaConfig, target: GiteaTarget): string { function remoteScript(action: "apply" | "status" | "validate" | "mirror-preflight" | "mirror-bootstrap" | "mirror-sync" | "mirror-status" | "mirror-webhook-apply" | "mirror-webhook-status", gitea: GiteaConfig, target: GiteaTarget, manifest: string, options: ApplyOptions | MirrorOptions, params: MirrorRemoteParams = {}): { script: string; payloadSummary: Record } { const frpcExposure = targetFrpcExposure(gitea, target); const sync = targetWebhookSync(gitea, target); + const selectedRepositories = params.repos ?? repositoriesForTarget(gitea, target); + const selectedCredentials = githubCredentialsForConsumers(gitea, target, selectedRepositories, false); + const defaultCredential = selectedCredentials[0] ?? gitea.sourceAuthority.credentials.github; const env: Record = { UNIDESK_GITEA_ACTION: action, UNIDESK_GITEA_TARGET_ID: target.id, @@ -1201,9 +1204,9 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-prefligh UNIDESK_GITEA_ROOT_URL: gitea.app.server.rootUrl, UNIDESK_GITEA_GITHUB_PROXY_ENABLED: gitea.sourceAuthority.githubProxy.enabled ? "1" : "0", UNIDESK_GITEA_GITHUB_PROXY_URL: gitea.sourceAuthority.githubProxy.url, - UNIDESK_GITEA_GITHUB_SECRET_KEY: gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key, - UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV: githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key), - UNIDESK_GITEA_GITHUB_SECRET_ENV_MAP: githubCredentialsForConsumers(gitea, target, repositoriesForTarget(gitea, target), true) + UNIDESK_GITEA_GITHUB_SECRET_KEY: defaultCredential.gitFetchCredential.secretRef.key, + UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV: githubTokenEnvName(defaultCredential.gitFetchCredential.secretRef.key), + UNIDESK_GITEA_GITHUB_SECRET_ENV_MAP: selectedCredentials .map((credential) => `${credential.gitFetchCredential.secretRef.key}=${githubTokenEnvName(credential.gitFetchCredential.secretRef.key)}`) .join(","), UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","), @@ -1702,10 +1705,7 @@ function webhookSyncSummary(gitea: GiteaConfig, target: GiteaTarget): Record> { const adminPath = credentialPath(gitea, gitea.sourceAuthority.credentials.admin.sourceRef); - const github = gitea.sourceAuthority.credentials.github; - const githubPath = credentialPath(gitea, github.sourceRef); const adminLinePair = existsSync(adminPath) ? parseLinePairCredential(adminPath, gitea.sourceAuthority.credentials.admin) : { username: null, password: null }; - const githubEnv = existsSync(githubPath) ? parseGithubCredentialFile(githubPath, github) : {}; const summaries: Array> = [ { id: "gitea-admin", @@ -1716,36 +1716,8 @@ function credentialSummaries(gitea: GiteaConfig, repositories = gitea.sourceAuth permissionResult: { status: "not-applicable", error: null }, valuesPrinted: false, }, - { - id: "github-upstream", - sourceRef: github.sourceRef, - present: existsSync(githubPath), - requiredKeysPresent: Boolean(githubEnv[github.sourceKey]), - fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]), - permissionResult: existsSync(githubPath) && Boolean(githubEnv[github.sourceKey]) - ? { status: "not-probed", error: null } - : { status: "blocked-missing-credential", error: "credential material is absent" }, - valuesPrinted: false, - }, ]; - for (const repo of repositories) { - const githubOverride = repo.credentialOverride?.github; - if (githubOverride === undefined) continue; - const sourcePath = credentialPath(gitea, githubOverride.sourceRef); - const values = existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, githubOverride) : {}; - summaries.push({ - id: `github-upstream:${repo.key}`, - repository: repo.upstream.repository, - sourceRef: githubOverride.sourceRef, - present: existsSync(sourcePath), - requiredKeysPresent: Boolean(values[githubOverride.sourceKey]), - fingerprint: fingerprintKeys(values, [githubOverride.sourceKey]), - permissionResult: existsSync(sourcePath) && Boolean(values[githubOverride.sourceKey]) - ? { status: "not-probed", permissions: githubOverride.permissions, error: null } - : { status: "blocked-missing-credential", permissions: githubOverride.permissions, error: "credential material is absent" }, - valuesPrinted: false, - }); - } + summaries.push(...buildGiteaMirrorBootstrapCredentialPreflight(gitea, repositories).credentials); return summaries; } @@ -1803,7 +1775,7 @@ function githubCredentialsForConsumers( repositories: GiteaMirrorRepository[], includeHookOwnerCredentials: boolean, ): GiteaGithubCredential[] { - const credentials = [gitea.sourceAuthority.credentials.github, ...repositories.flatMap((repo) => repo.credentialOverride?.github ?? [])]; + const credentials = repositories.map((repo) => githubCredentialForRepo(gitea, repo)); if (includeHookOwnerCredentials && gitea.sourceAuthority.webhookSync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase()) { credentials.push(...gitea.sourceAuthority.repositories.flatMap((repo) => { 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 bc0a0982..99dd5965 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -133,7 +133,7 @@ test("unselected parse and repository errors remain warnings while selected obje evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u), }); expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-test-nc01")).toBe(false); - expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl"); + expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).not.toThrow(); const duplicateSelectedRepository = pacConfigDocument(); const repositories = duplicateSelectedRepository.repositories as Array>; diff --git a/scripts/src/platform-infra-pipelines-as-code-source-artifact.ts b/scripts/src/platform-infra-pipelines-as-code-source-artifact.ts index ba6054f8..aaae95b3 100644 --- a/scripts/src/platform-infra-pipelines-as-code-source-artifact.ts +++ b/scripts/src/platform-infra-pipelines-as-code-source-artifact.ts @@ -19,10 +19,10 @@ import { stableJsonSha256, stableJsonValue } from "./stable-json"; import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance"; import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline"; import { renderSelfMediaDesiredPipeline, selfMediaSourceWorktreeRemote } from "./selfmedia-delivery-renderer"; -import { renderPikaoaTestDesiredPipeline, pikaoaTestSourceWorktreeRemote } from "./pikaoa-test-delivery-renderer"; +import { renderPikaoaTestDesiredPipeline, pikaoaReleaseSourceWorktreeRemote, pikaoaTestSourceWorktreeRemote } from "./pikaoa-test-delivery-renderer"; export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation"; -export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-test-runtime"; +export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-test-runtime" | "pikaoa-release-runtime"; export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime"; export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable"; @@ -593,7 +593,7 @@ function renderPikaoaTestPipeline(binding: PacSourceArtifactBinding, sourceWorkt provenance: { configRef: rendered.configRef, effectiveConfigSha256: rendered.effectiveConfigSha256, - renderer: "pikaoa-test-runtime", + renderer: binding.consumer.sourceArtifact.renderer, mode: "embedded-pipeline-spec", }, }; @@ -852,7 +852,7 @@ 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" || binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) { + if ((binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) { return { name, value: param.default }; } if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) { @@ -867,7 +867,7 @@ export function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desired 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" || (name === "workspace" && binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime")) { + if (name === "source" || (name === "workspace" && (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) { return { name, volumeClaimTemplate: { @@ -946,6 +946,7 @@ function owningSourceRemote(binding: PacSourceArtifactBinding): string { if (binding.consumer.sourceArtifact.renderer === "sub2rank-platform-service") return sub2RankOwningSourceRemote(); if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime") return selfMediaSourceWorktreeRemote(binding.consumer.node); if (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime") return pikaoaTestSourceWorktreeRemote(binding.consumer.node); + if (binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") return pikaoaReleaseSourceWorktreeRemote(binding.consumer.node); if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved"); return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl; } @@ -1065,7 +1066,8 @@ export function pacSourceArtifactProvenanceFromManifest(manifest: Record, path: s function parseSourceArtifact(value: Record, path: string): PacSourceArtifactSpec { const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const); - const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-test-runtime"] as const); + const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-test-runtime", "pikaoa-release-runtime"] as const); const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`); const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`); const taskRunTemplate = y.objectField(value, "taskRunTemplate", path); @@ -812,6 +812,7 @@ function parseSourceArtifact(value: Record, path: string): PacS if (renderer === "sub2rank-platform-service" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer sub2rank-platform-service requires embedded-pipeline-spec`); if (renderer === "selfmedia-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer selfmedia-runtime requires embedded-pipeline-spec`); if (renderer === "pikaoa-test-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-test-runtime requires embedded-pipeline-spec`); + if (renderer === "pikaoa-release-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-release-runtime requires embedded-pipeline-spec`); return { mode, renderer,