feat: 建立 PikaOA 稳定开发自动交付运行面

This commit is contained in:
Codex
2026-07-16 08:01:39 +02:00
parent 22b42a280d
commit 902c619267
21 changed files with 756 additions and 2843 deletions
-175
View File
@@ -1,175 +0,0 @@
version: 1
kind: pikaoa-platform-delivery
metadata:
id: pikaoa-enterprise-fixture
owner: unidesk
repository: pikainc/pikaoa
spec: PJ2026-03
testRuntime:
defaultTargetId: TEST01
targets:
TEST01:
enabled: true
validationOnly: true
node: TEST01
route: TEST01:k3s
namespace: pikaoa-test
ttlSeconds: 7200
waitTimeoutSeconds: 180
fieldManager: unidesk-pikaoa-test-target
taskState:
rootPath: /tmp/unidesk-pikaoa-test-target-fixture
statusRequestTimeoutSeconds: 3
logTailLines: 8
cleanup:
mode: delete-namespace
requireOwnershipLabels: true
source:
mode: prebuilt-images
repository: pikainc/pikaoa
commitPolicy: cli-required
branch: master
worktreeRemote: https://github.com/pikainc/pikaoa.git
readUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
snapshotPrefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-test01
images:
api: registry.invalid/pikaoa/api:${commit}
worker: registry.invalid/pikaoa/worker:${commit}
web: registry.invalid/pikaoa/web:${commit}
initializer: registry.invalid/pikaoa/api:${commit}
pullPolicy: IfNotPresent
ci:
lane: pikaoa-test
namespace: pikaoa-ci
pipeline: pikaoa-test-test01-pac
pipelineRunPrefix: pikaoa-test-test01
serviceAccountName: pikaoa-test-test01-tekton-runner
workspaceSize: 1Gi
pipelineTimeout: 20m0s
toolsImage: registry.invalid/tools:latest
buildkitImage: registry.invalid/buildkit:rootless
build:
dockerfiles:
api: deploy/api.Dockerfile
worker: deploy/worker.Dockerfile
web: deploy/web.Dockerfile
images:
api: registry.invalid/pikaoa/api
worker: registry.invalid/pikaoa/worker
web: registry.invalid/pikaoa/web
networkMode: host
gitops:
readUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
writeUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
branch: test01-pikaoa-test-gitops
manifestPath: deploy/gitops/test01-test/resources.yaml
credential:
secretName: pac-gitea-pikaoa-test-test01
tokenKey: token
username: unidesk-admin
author:
name: PikaOA Test Fixture
email: pikaoa-test-fixture@unidesk.local
database:
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
name: pikaoa_test
username: pikaoa_test
schema: pikaoa
connection:
sourceRef: ./secrets/pikaoa-test.env
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
workerMetricsPort: 8081
webPort: 8080
webApiUpstreamEnv: PIKAOA_API_UPSTREAM
attachment:
storageRoot: /var/lib/pikaoa/attachments
maxBytes: 104857600
claimName: pikaoa-attachments
storageRequest: 1Gi
storageClassName: null
fsGroup: 65532
accessModes:
- ReadWriteOnce
workerInterval: 2s
outbox:
batchSize: 50
maxAttempts: 4
retryDelay: 2s
sessionTTL: 8h
shutdownTimeout: 10s
administrator:
username: admin
password:
sourceRef: ./secrets/pikaoa-test-admin-password.txt
targetKey: PIKAOA_BOOTSTRAP_ADMIN_PASSWORD
employee:
username: employee
password:
sourceRef: ./secrets/pikaoa-test-employee-password.txt
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
sessionSecret:
sourceRef: ./secrets/pikaoa-test.env
sourceKey: PIKAOA_SESSION_SECRET
targetKey: PIKAOA_SESSION_SECRET
adminToken:
sourceRef: ./secrets/pikaoa-test.env
sourceKey: OA_ADMIN_TOKEN
targetKey: OA_ADMIN_TOKEN
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: 192.0.2.10
port: 32080
observability:
otlpEndpoint: otel-collector.observability.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:
PROD:
namespace: pikaoa
ci:
namespace: pikaoa-ci
+26 -53
View File
@@ -11,6 +11,7 @@ metadata:
- 2110
- 2128
- 2263
- 2274
defaults:
targetId: NC01
@@ -29,45 +30,30 @@ modules:
events: transactional-outbox
workers: named-consumer
testRuntime:
developmentRuntime:
defaultTargetId: NC01
targets:
NC01:
enabled: true
validationOnly: false
node: NC01
route: NC01:k3s
namespace: pikaoa-test
ttlSeconds: 86400
waitTimeoutSeconds: 300
fieldManager: unidesk-pikaoa-test-target
taskState:
rootPath: /var/lib/unidesk/pikaoa-test-target
statusRequestTimeoutSeconds: 10
logTailLines: 80
cleanup:
mode: delete-namespace
requireOwnershipLabels: true
namespace: pikaoa-dev
fieldManager: unidesk-pikaoa-development-runtime
source:
mode: prebuilt-images
mode: pac-source-build
repository: pikainc/pikaoa
commitPolicy: cli-required
commitPolicy: pac-revision-required
branch: master
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-master-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-test
lane: pikaoa-dev
namespace: pikaoa-ci
pipeline: pikaoa-test-nc01-pac
pipelineRunPrefix: pikaoa-test-nc01
serviceAccountName: pikaoa-test-nc01-tekton-runner
pipeline: pikaoa-dev-nc01-pac
pipelineRunPrefix: pikaoa-dev-nc01
serviceAccountName: pikaoa-dev-nc01-tekton-runner
workspaceSize: 8Gi
pipelineTimeout: 20m0s
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
@@ -85,22 +71,22 @@ testRuntime:
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-test-gitops
manifestPath: deploy/gitops/nc01-test/resources.yaml
branch: nc01-pikaoa-dev-gitops
manifestPath: deploy/gitops/nc01-dev/resources.yaml
credential:
secretName: pac-gitea-pikaoa-test-nc01
secretName: pac-gitea-pikaoa-dev-nc01
tokenKey: token
username: unidesk-admin
author:
name: PikaOA Test NC01 CI
email: pikaoa-test-nc01-ci@unidesk.local
name: PikaOA Development NC01 CI
email: pikaoa-dev-nc01-ci@unidesk.local
database:
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
name: pikaoa_test
username: pikaoa_test
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-dev-database-url
name: pikaoa_dev
username: pikaoa_dev
schema: pikaoa
connection:
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
sourceKey: DATABASE_URL
targetKey: DATABASE_URL
runtime:
@@ -113,7 +99,8 @@ testRuntime:
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
secretName: pikaoa-dev-runtime
configMapName: pikaoa-runtime-config
apiServiceName: pikaoa-api
apiPort: 8080
workerMetricsPort: 8081
@@ -146,11 +133,11 @@ testRuntime:
sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
sessionSecret:
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
sourceKey: PIKAOA_SESSION_SECRET
targetKey: PIKAOA_SESSION_SECRET
adminToken:
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
sourceKey: OA_ADMIN_TOKEN
targetKey: OA_ADMIN_TOKEN
initializer:
@@ -197,33 +184,18 @@ releaseRuntime:
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
mode: pac-source-build
repository: pikainc/pikaoa
commitPolicy: cli-required
commitPolicy: pac-revision-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
@@ -277,6 +249,7 @@ releaseRuntime:
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
configMapName: pikaoa-runtime-config
apiServiceName: pikaoa-api
apiPort: 8080
workerMetricsPort: 8081
+22 -21
View File
@@ -8,6 +8,7 @@ metadata:
relatedIssues:
- 1875
- 2250
- 2274
cluster:
role: primary
@@ -126,8 +127,8 @@ postgres:
address: 10.42.0.0/16
method: scram-sha-256
- type: hostssl
database: pikaoa_test
user: pikaoa_test
database: pikaoa_dev
user: pikaoa_dev
address: 10.42.0.0/16
method: scram-sha-256
@@ -191,20 +192,20 @@ secrets:
PIKAOA_DB_NAME: pikaoa
randomHex:
PIKAOA_DB_PASSWORD: 32
- name: pikaoa-test-nc01-db-credentials
sourceRef: platform-db/pikaoa-test-db.env
- name: pikaoa-dev-nc01-db-credentials
sourceRef: platform-db/pikaoa-dev-db.env
type: env
requiredKeys:
- PIKAOA_TEST_DB_USER
- PIKAOA_TEST_DB_PASSWORD
- PIKAOA_TEST_DB_NAME
- PIKAOA_DEV_DB_USER
- PIKAOA_DEV_DB_PASSWORD
- PIKAOA_DEV_DB_NAME
createIfMissing:
enabled: true
values:
PIKAOA_TEST_DB_USER: pikaoa_test
PIKAOA_TEST_DB_NAME: pikaoa_test
PIKAOA_DEV_DB_USER: pikaoa_dev
PIKAOA_DEV_DB_NAME: pikaoa_dev
randomHex:
PIKAOA_TEST_DB_PASSWORD: 32
PIKAOA_DEV_DB_PASSWORD: 32
objects:
roles:
@@ -244,10 +245,10 @@ objects:
createdb: false
createrole: false
superuser: false
- name: pikaoa_test
- name: pikaoa_dev
passwordRef:
sourceRef: platform-db/pikaoa-test-db.env
key: PIKAOA_TEST_DB_PASSWORD
sourceRef: platform-db/pikaoa-dev-db.env
key: PIKAOA_DEV_DB_PASSWORD
login: true
attributes:
createdb: false
@@ -274,8 +275,8 @@ objects:
encoding: UTF8
locale: C.UTF-8
extensions: []
- name: pikaoa_test
owner: pikaoa_test
- name: pikaoa_dev
owner: pikaoa_dev
encoding: UTF8
locale: C.UTF-8
extensions: []
@@ -349,20 +350,20 @@ exports:
- scope: pikaoa
secret: pikaoa-runtime
key: DATABASE_URL
- name: pikaoa-test-database-url
sourceSecretRef: platform-db/pikaoa-test-db.env
- name: pikaoa-dev-database-url
sourceSecretRef: platform-db/pikaoa-dev-db.env
render:
envKey: DATABASE_URL
format: postgresql://$(PIKAOA_TEST_DB_USER):$(PIKAOA_TEST_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_TEST_DB_NAME)?sslmode=require
format: postgresql://$(PIKAOA_DEV_DB_USER):$(PIKAOA_DEV_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_DEV_DB_NAME)?sslmode=require
variables:
PGHOST: 10.42.0.1
writeToSecretSource:
sourceRef: platform-infra/pikaoa-test.env
sourceRef: platform-infra/pikaoa-dev.env
key: DATABASE_URL
mode: update-or-insert
consumers:
- scope: pikaoa-test
secret: pikaoa-test-runtime
- scope: pikaoa-dev
secret: pikaoa-dev-runtime
key: DATABASE_URL
backup:
+3 -2
View File
@@ -13,6 +13,7 @@ metadata:
- 1560
- 1760
- 1768
- 2274
defaults:
targetId: NC01
@@ -489,7 +490,7 @@ sourceAuthority:
naming: gitea-actions-immutable-source
prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
legacyGitMirror: null
- key: pikaoa-test-nc01
- key: pikaoa-dev-nc01
targetId: NC01
credentialOverride:
github:
@@ -529,7 +530,7 @@ sourceAuthority:
publicRead: false
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
gitops:
branch: nc01-pikaoa-test-gitops
branch: nc01-pikaoa-dev-gitops
flushDisposition: gitea-writeback
snapshot:
naming: gitea-actions-immutable-source
+31 -30
View File
@@ -14,6 +14,7 @@ metadata:
- 1769
- 1802
- 1811
- 2274
defaults:
targetId: NC01
consumerId: hwlab-nc01-v03
@@ -247,15 +248,15 @@ repositories:
runtime_service_port: "4317"
health_path: /healthz
health_url: http://selfmedia.selfmedia.svc.cluster.local:4317/healthz
- id: pikaoa-test-nc01
name: pikaoa-test-nc01
- id: pikaoa-dev-nc01
name: pikaoa-dev-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-test-nc01
secretName: pac-gitea-pikaoa-dev-nc01
tokenKey: token
webhookSecretKey: webhook.secret
concurrencyLimit: 1
@@ -264,9 +265,9 @@ repositories:
source_branch: master
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-nc01
node: NC01
pipeline_name: pikaoa-test-nc01-pac
pipeline_run_prefix: pikaoa-test-nc01
service_account: pikaoa-test-nc01-tekton-runner
pipeline_name: pikaoa-dev-nc01-pac
pipeline_run_prefix: pikaoa-dev-nc01
service_account: pikaoa-dev-nc01-tekton-runner
pipeline_timeout: 20m0s
workspace_pvc_size: 8Gi
image_repository: 127.0.0.1:5000/pikaoa/api
@@ -276,16 +277,16 @@ repositories:
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-test-gitops
gitops_branch: nc01-pikaoa-dev-gitops
gitops_username: unidesk-admin
gitops_secret_name: pac-gitea-pikaoa-test-nc01
gitops_manifest_path: deploy/gitops/nc01-test/resources.yaml
runtime_namespace: pikaoa-test
gitops_secret_name: pac-gitea-pikaoa-dev-nc01
gitops_manifest_path: deploy/gitops/nc01-dev/resources.yaml
runtime_namespace: pikaoa-dev
runtime_deployment: pikaoa-api
runtime_service: pikaoa-web
runtime_service_port: "8080"
health_path: /healthz
health_url: http://pikaoa-web.pikaoa-test.svc.cluster.local:8080/healthz
health_url: http://pikaoa-web.pikaoa-dev.svc.cluster.local:8080/healthz
consumers:
- extends: templates.consumers.agentrunV02
variables:
@@ -560,49 +561,49 @@ consumers:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
fsGroup: 1000
- id: pikaoa-test-nc01
repositoryRef: pikaoa-test-nc01
- id: pikaoa-dev-nc01
repositoryRef: pikaoa-dev-nc01
node: NC01
lane: pikaoa-test
lane: pikaoa-dev
namespace: pikaoa-ci
pipeline: pikaoa-test-nc01-pac
pipelineRunPrefix: pikaoa-test-nc01
pipeline: pikaoa-dev-nc01-pac
pipelineRunPrefix: pikaoa-dev-nc01
argoNamespace: argocd
argoApplication: pikaoa-test-nc01
argoApplication: pikaoa-dev-nc01
closeoutGitOpsMirrorFlush: false
argoBootstrap:
project: default
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
targetRevision: nc01-pikaoa-test-gitops
path: deploy/gitops/nc01-test
destinationNamespace: pikaoa-test
targetRevision: nc01-pikaoa-dev-gitops
path: deploy/gitops/nc01-dev
destinationNamespace: pikaoa-dev
automated: true
repositoryCredential:
secretName: argocd-repo-pikaoa-test-nc01
secretName: argocd-repo-pikaoa-dev-nc01
username: unidesk-admin
deliveryProvenance:
required: true
markerValue: admission-pac-v2:pikaoa-test-nc01
executionServiceAccountName: pikaoa-test-nc01-tekton-runner
markerValue: admission-pac-v2:pikaoa-dev-nc01
executionServiceAccountName: pikaoa-dev-nc01-tekton-runner
gitOps:
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
targetRevision: nc01-pikaoa-test-gitops
targetRevision: nc01-pikaoa-dev-gitops
runnerServiceAccount:
name: pikaoa-test-nc01-tekton-runner
name: pikaoa-dev-nc01-tekton-runner
automountServiceAccountToken: false
roleBindingName: pikaoa-test-nc01-tekton-runner
roleBindingName: pikaoa-dev-nc01-tekton-runner
sourceArtifact:
mode: embedded-pipeline-spec
renderer: pikaoa-test-runtime
configRef: config/pikaoa.yaml#testRuntime.targets.NC01
pipelineRunPath: .tekton/pikaoa-test-nc01-pac.yaml
renderer: pikaoa-development-runtime
configRef: config/pikaoa.yaml#developmentRuntime.targets.NC01
pipelineRunPath: .tekton/pikaoa-dev-nc01-pac.yaml
maxKeepRuns: 8
taskRunTemplate:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
fsGroup: 1000
- id: pikaoa-nc01
repositoryRef: pikaoa-test-nc01
repositoryRef: pikaoa-dev-nc01
node: NC01
lane: pikaoa
namespace: pikaoa-ci
+2 -1
View File
@@ -6,6 +6,7 @@ metadata:
owner: platform-infra
relatedIssues:
- 2181
- 2274
defaults:
targetId: NC01
@@ -37,7 +38,7 @@ targets:
sites:
- id: pikaoa-development
configRef: config/pikaoa.yaml
path: testRuntime.targets.NC01
path: developmentRuntime.targets.NC01
- id: pikaoa-production
configRef: config/pikaoa.yaml
path: releaseRuntime.targets.NC01
+11 -10
View File
@@ -13,6 +13,7 @@ metadata:
- 2110
- 2128
- 2263
- 2274
sources:
root: /root/.unidesk/.state/secrets
@@ -116,7 +117,7 @@ sources:
RESTIC_PASSWORD:
bytes: 32
prefix: poa_restic_
- sourceRef: platform-infra/pikaoa-test.env
- sourceRef: platform-infra/pikaoa-dev.env
type: env
requiredKeys:
- DATABASE_URL
@@ -266,10 +267,10 @@ targets:
namespace: pikaoa
scope: pikaoa
enabled: true
- id: pikaoa-test-nc01
- id: pikaoa-dev-nc01
route: NC01:k3s
namespace: pikaoa-test
scope: pikaoa-test
namespace: pikaoa-dev
scope: pikaoa-dev
enabled: true
kubernetesSecrets:
@@ -301,18 +302,18 @@ kubernetesSecrets:
- sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
sourceKey: contents
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
- name: pikaoa-test-runtime
targetId: pikaoa-test-nc01
secretName: pikaoa-test-runtime
- name: pikaoa-dev-runtime
targetId: pikaoa-dev-nc01
secretName: pikaoa-dev-runtime
type: Opaque
data:
- sourceRef: platform-infra/pikaoa-test.env
- sourceRef: platform-infra/pikaoa-dev.env
sourceKey: DATABASE_URL
targetKey: DATABASE_URL
- sourceRef: platform-infra/pikaoa-test.env
- sourceRef: platform-infra/pikaoa-dev.env
sourceKey: PIKAOA_SESSION_SECRET
targetKey: PIKAOA_SESSION_SECRET
- sourceRef: platform-infra/pikaoa-test.env
- sourceRef: platform-infra/pikaoa-dev.env
sourceKey: OA_ADMIN_TOKEN
targetKey: OA_ADMIN_TOKEN
- sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt
+11 -2
View File
@@ -1,5 +1,6 @@
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
// SPEC: PJ2026-03 PikaOA v0.8.
// UniDesk CLI dispatcher with bounded server lifecycle and job drill-down output.
import { readConfig } from "./src/config";
import { debugDispatch, debugEgressProxy, debugHealth, debugProviderRootfs, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
@@ -429,8 +430,16 @@ async function main(): Promise<void> {
}
if (top === "pikaoa") {
const { runPikaoaCommand } = await import("./src/pikaoa-test-target");
const result = await runPikaoaCommand(readConfig(), args.slice(1));
if (args[1] !== "attachments-backup") {
throw new CliInputError("pikaoa 只支持 attachments-backup;运行面由 PaC 自动交付", {
code: "unsupported-pikaoa-command",
argument: args[1] ?? "<missing>",
supported: ["attachments-backup"],
usage: "bun scripts/cli.ts pikaoa attachments-backup --help",
});
}
const { runPikaoaAttachmentsBackupCommand } = await import("./src/pikaoa-attachments-backup");
const result = await runPikaoaAttachmentsBackupCommand(readConfig(), args.slice(2));
if (isRenderedCliResult(result)) {
emitText(result.renderedText, result.command || commandName);
if (!result.ok) process.exitCode = 1;
+3 -3
View File
@@ -1,5 +1,6 @@
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
// SPEC: PJ2026-03 PikaOA v0.8.
// Static CLI help for job aliases and server lifecycle progressive disclosure.
import { join } from "node:path";
import { ghHelp, ghScopedHelp } from "./gh";
@@ -75,7 +76,7 @@ export function rootHelp(): unknown {
{ command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node NC01 --lane v03", description: "Operate the NC01 HWLAB v0.3 development and deployment lane from YAML source truth." },
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
{ command: "platform-infra sub2api|langbot|n8n|webterm|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot, n8n and Web Terminal, manage YAML-controlled public Caddy/FRP exposure and WeChat archive workflows, and inspect status/logs without printing secrets." },
{ command: "pikaoa test-target ... | pikaoa attachments-backup ...", description: "管理 PikaOA YAML-first 测试运行面和生产附件离机备份控制面。" },
{ command: "pikaoa attachments-backup ...", description: "管理 PikaOA 生产附件离机备份控制面;开发与生产运行面由 PaC 自动交付。" },
{ command: "secrets plan|sync|status", description: "Plan, push and inspect YAML-declared local secret source keys to Kubernetes Secrets without printing or reverse-engineering values." },
{ command: "platform-db postgres plan|status|export-secrets|apply", description: "Manage YAML-declared host-native PostgreSQL 16 on PK01 for Sub2API/platform state, with PostgreSQL native TLS and redacted secret exports." },
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Archived HWLAB DEV CD diagnostics only; current HWLAB rollout uses NC01 GitOps." },
@@ -1065,9 +1066,8 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
);
}
if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary());
if (top === "pikaoa" && sub === "test-target") return loadHelp(async () => (await import("./pikaoa-test-target")).pikaoaTestTargetHelp(), { command: "pikaoa test-target", source: "config/pikaoa.yaml#testRuntime" });
if (top === "pikaoa" && sub === "attachments-backup") return loadHelp(async () => (await import("./pikaoa-attachments-backup")).pikaoaAttachmentsBackupHelp(), { command: "pikaoa attachments-backup", source: "config/pikaoa.yaml#delivery.targets.*.attachmentsBackup" });
if (top === "pikaoa") return loadHelp(async () => (await import("./pikaoa-test-target")).pikaoaHelp(), { command: "pikaoa", commands: ["test-target", "attachments-backup"] });
if (top === "pikaoa" && (sub === undefined || isHelpToken(sub))) return loadHelp(async () => ({ ok: true, commands: [(await import("./pikaoa-attachments-backup")).pikaoaAttachmentsBackupHelp()] }), { command: "pikaoa", commands: ["attachments-backup"] });
if (top === "platform-db") return platformDbHelp();
if (top === "secrets") return secretsHelp();
if (top === "web-probe") return loadHelp(async () => (await import("./web-probe")).webProbeHelp(), webProbeHelpSummary());
@@ -3,13 +3,13 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { renderPikaoaTestDesiredPipeline } from "./pikaoa-test-delivery-renderer";
import { renderPikaoaDesiredPipeline } from "./pikaoa-delivery-renderer";
function pacDocument(): Record<string, unknown> {
return Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
}
function rendererBinding(id = "pikaoa-test-nc01"): Parameters<typeof renderPikaoaTestDesiredPipeline>[0] {
function rendererBinding(id = "pikaoa-dev-nc01"): Parameters<typeof renderPikaoaDesiredPipeline>[0] {
const document = pacDocument();
const repositories = document.repositories as Array<Record<string, unknown>>;
const consumers = document.consumers as Array<Record<string, unknown>>;
@@ -25,31 +25,31 @@ function rendererBinding(id = "pikaoa-test-nc01"): Parameters<typeof renderPikao
...((consumer.params as Record<string, unknown> | undefined) ?? {}),
},
},
} as Parameters<typeof renderPikaoaTestDesiredPipeline>[0];
} as Parameters<typeof renderPikaoaDesiredPipeline>[0];
}
function sourceFixture(): string {
const root = mkdtempSync(resolve(tmpdir(), "pikaoa-test-renderer-"));
const root = mkdtempSync(resolve(tmpdir(), "pikaoa-development-renderer-"));
mkdirSync(resolve(root, "deploy"), { recursive: true });
for (const file of ["api.Dockerfile", "worker.Dockerfile", "web.Dockerfile"]) writeFileSync(resolve(root, "deploy", file), "FROM scratch\n");
return root;
}
describe("PikaOA test PaC renderer", () => {
describe("PikaOA development PaC renderer", () => {
test("shares one PaC repository while isolating master and release consumers", () => {
const document = pacDocument();
const repositories = document.repositories as Array<Record<string, unknown>>;
const consumers = document.consumers as Array<Record<string, unknown>>;
const repository = repositories.find((item) => item.id === "pikaoa-test-nc01");
const testConsumer = consumers.find((item) => item.id === "pikaoa-test-nc01");
const repository = repositories.find((item) => item.id === "pikaoa-dev-nc01");
const developmentConsumer = consumers.find((item) => item.id === "pikaoa-dev-nc01");
const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01");
const duplicateProductionRepository = repositories.find((item) => item.id === "pikaoa-nc01");
expect((repository?.params as Record<string, unknown>).source_branch).toBe("master");
expect((testConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-test-nc01-pac.yaml");
expect((testConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-test-runtime");
expect((developmentConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-dev-nc01-pac.yaml");
expect((developmentConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-development-runtime");
expect((productionConsumer?.params as Record<string, unknown>).source_branch).toBe("release");
expect((productionConsumer?.params as Record<string, unknown>).source_snapshot_prefix).toBe("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
expect(productionConsumer?.repositoryRef).toBe("pikaoa-test-nc01");
expect(productionConsumer?.repositoryRef).toBe("pikaoa-dev-nc01");
expect(duplicateProductionRepository).toBeUndefined();
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-release-runtime");
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
@@ -58,13 +58,13 @@ describe("PikaOA test PaC renderer", () => {
test("renders release runtime from its isolated owning selector", () => {
const source = sourceFixture();
try {
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding("pikaoa-nc01"), source);
const rendered = renderPikaoaDesiredPipeline(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");
expect(serialized).not.toContain("pikaoa-dev-nc01-pac");
} finally {
rmSync(source, { recursive: true, force: true });
}
@@ -73,7 +73,7 @@ describe("PikaOA test PaC renderer", () => {
test("renders parallel digest builds and a PreSync fresh database initializer", () => {
const source = sourceFixture();
try {
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding(), source);
const rendered = renderPikaoaDesiredPipeline(rendererBinding(), source);
const spec = rendered.pipeline.spec as Record<string, unknown>;
const tasks = spec.tasks as Array<Record<string, unknown>>;
for (const task of tasks) {
@@ -97,6 +97,8 @@ describe("PikaOA test PaC renderer", () => {
expect(buildScript).not.toContain("\u0001");
expect(buildScript).toContain("Docker-Content-Digest:");
expect(buildScript).toContain("build_status=$?");
expect(buildScript).toContain('if [ -f "$metadata" ]');
expect(buildScript).toContain("grep -Eq '^sha256:[0-9a-f]{64}$'");
expect(buildScript).toContain('"code":"buildctl-nonzero-artifact-verified"');
expect(buildScript).toContain('"artifactVerified":true');
}
@@ -137,11 +139,17 @@ describe("PikaOA test PaC renderer", () => {
expect(manifest).toContain("name: pikaoa-init");
expect(manifest).toContain("name: initializer");
expect(manifest).not.toContain("kind: Secret");
expect(manifest).toContain("secretName: pikaoa-test-runtime");
expect(manifest).toContain("kind: ConfigMap");
expect(manifest).toContain("name: pikaoa-runtime-config");
expect(manifest).toContain("configMap:");
expect(manifest).toContain("secretKeyRef:");
expect(manifest.match(/name: pikaoa-dev-runtime/gu)).toHaveLength(7);
expect(manifest).not.toContain("secretName:");
expect(manifest.match(/name: DATABASE_URL/gu)).toHaveLength(3);
expect(manifest.match(/key: DATABASE_URL/gu)).toHaveLength(3);
expect(manifest.match(/name: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
expect(manifest.match(/key: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
expect(script).toContain("grep -Eq '@sha256:[0-9a-f]{64}$'");
expect(manifest).toContain("image: __PIKAOA_API_IMAGE__");
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
@@ -1,17 +1,18 @@
// SPEC: PJ2026-03 PikaOA v0.8.
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import {
pikaoaTestRuntimeConfigRef,
pikaoaDevelopmentRuntimeConfigRef,
pikaoaReleaseRuntimeConfigRef,
resolvePikaoaReleaseRuntimeTargetByConfigRef,
resolvePikaoaTestRuntimeTargetByConfigRef,
type TestTargetSpec,
} from "./pikaoa-test-target";
import { renderPikaoaTestRuntimeManifest, type PikaoaTestRenderContext } from "./pikaoa-test-runtime-manifest";
resolvePikaoaDevelopmentRuntimeTargetByConfigRef,
type PikaoaRuntimeSpec,
} from "./pikaoa-runtime";
import { renderPikaoaRuntimeManifest, type PikaoaRuntimeRenderContext } from "./pikaoa-runtime-manifest";
import { stableJsonSha256 } from "./stable-json";
export interface PikaoaTestRendererBinding {
export interface PikaoaRendererBinding {
readonly consumer: {
readonly id: string;
readonly node: string;
@@ -31,20 +32,20 @@ export interface PikaoaTestRendererBinding {
};
}
export interface PikaoaTestRenderedPipeline {
export interface PikaoaRenderedPipeline {
readonly pipeline: Record<string, unknown>;
readonly configRef: string;
readonly effectiveConfigSha256: string;
readonly sourceWorktreeRemote: string;
}
export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBinding, sourceWorktree: string): PikaoaTestRenderedPipeline {
export function renderPikaoaDesiredPipeline(binding: PikaoaRendererBinding, sourceWorktree: string): PikaoaRenderedPipeline {
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") {
: resolvePikaoaDevelopmentRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaDevelopmentRuntimeConfigRef(target.id);
if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-development-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) {
@@ -60,15 +61,15 @@ export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBindi
};
}
export function pikaoaTestSourceWorktreeRemote(targetId: string): string {
return resolvePikaoaTestRuntimeTargetByConfigRef(pikaoaTestRuntimeConfigRef(targetId)).source.worktreeRemote;
export function pikaoaDevelopmentSourceWorktreeRemote(targetId: string): string {
return resolvePikaoaDevelopmentRuntimeTargetByConfigRef(pikaoaDevelopmentRuntimeConfigRef(targetId)).source.worktreeRemote;
}
export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string {
return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote;
}
function pipeline(target: TestTargetSpec): Record<string, unknown> {
function pipeline(target: PikaoaRuntimeSpec): Record<string, unknown> {
const runtimeManifest = renderGitOpsManifest(target);
return {
apiVersion: "tekton.dev/v1",
@@ -96,7 +97,7 @@ function pipeline(target: TestTargetSpec): Record<string, unknown> {
};
}
function sourceTask(target: TestTargetSpec): Record<string, unknown> {
function sourceTask(target: PikaoaRuntimeSpec): Record<string, unknown> {
return {
name: "source",
taskSpec: {
@@ -157,7 +158,7 @@ printf '{"ok":true,"phase":"source","sourceCommit":"%s","valuesPrinted":false}\n
};
}
function imageBuildTask(target: TestTargetSpec, component: "api" | "worker" | "web"): Record<string, unknown> {
function imageBuildTask(target: PikaoaRuntimeSpec, component: "api" | "worker" | "web"): Record<string, unknown> {
const taskName = `build-${component}`;
const imageRepository = target.build.images[component];
const dockerfile = target.build.dockerfiles[component];
@@ -190,7 +191,10 @@ buildctl-daemonless.sh build \\
--metadata-file "$metadata"
build_status=$?
set -e
metadata_compact=$(tr -d '\\n\\r' < "$metadata")
metadata_compact=
if [ -f "$metadata" ]; then
metadata_compact=$(tr -d '\\n\\r' < "$metadata")
fi
digest=$(printf '%s' "$metadata_compact" | sed -n 's/.*"containerimage.digest"[[:space:]]*:[[:space:]]*"\\(sha256:[0-9a-f]\\{64\\}\\)".*/\\1/p' | head -n1)
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
registry_host=${shellSingleQuote(imageRepository.split("/")[0] ?? "")}
@@ -198,7 +202,10 @@ if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
headers=$(wget --server-response --spider --header='Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' "http://$registry_host/v2/$registry_path/manifests/$revision" 2>&1 || true)
digest=$(printf '%s\\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^[[:space:]]*Docker-Content-Digest:/ {gsub(/\\r/, "", $2); print $2; exit}')
fi
case "$digest" in sha256:[0-9a-f][0-9a-f]*) test "\${#digest}" -eq 71 ;; *) echo 'build digest missing from metadata and registry manifest' >&2; exit 3;; esac
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
echo 'build digest missing from metadata and registry manifest' >&2
exit 3
fi
if [ "$build_status" -ne 0 ]; then
printf '{"level":"warning","code":"buildctl-nonzero-artifact-verified","component":"${component}","buildctlExitCode":%s,"artifactVerified":true,"valuesPrinted":false}\\n' "$build_status" >&2
fi
@@ -214,7 +221,7 @@ printf '{"ok":true,"phase":"${taskName}","sourceCommit":"%s","imageTag":"%s","di
};
}
function gitOpsTask(target: TestTargetSpec, runtimeManifest: string): Record<string, unknown> {
function gitOpsTask(target: PikaoaRuntimeSpec, runtimeManifest: string): Record<string, unknown> {
const manifestB64 = Buffer.from(runtimeManifest, "utf8").toString("base64");
return {
name: "gitops-publish",
@@ -257,7 +264,10 @@ api_image="$(params.api-image)"
worker_image="$(params.worker-image)"
web_image="$(params.web-image)"
for image in "$api_image" "$worker_image" "$web_image"; do
case "$image" in *@sha256:[0-9a-f]*) ;; *) echo 'required digestRef missing' >&2; exit 4;; esac
if ! printf '%s' "$image" | grep -Eq '@sha256:[0-9a-f]{64}$'; then
echo 'required digestRef missing' >&2
exit 4
fi
done
rm -rf /workspace/gitops
mkdir -p /workspace/gitops
@@ -320,25 +330,22 @@ printf '{"ok":true,"status":"published","phase":"gitops-publish","sourceCommitId
};
}
function renderGitOpsManifest(target: TestTargetSpec): string {
const context: PikaoaTestRenderContext = {
function renderGitOpsManifest(target: PikaoaRuntimeSpec): string {
const context: PikaoaRuntimeRenderContext = {
instanceId: "default",
commit: "__PIKAOA_SOURCE_COMMIT__",
step: "all",
namespace: target.namespace,
expiresAt: "gitops-managed",
apiImage: "__PIKAOA_API_IMAGE__",
workerImage: "__PIKAOA_WORKER_IMAGE__",
webImage: "__PIKAOA_WEB_IMAGE__",
initializerImage: "__PIKAOA_API_IMAGE__",
};
const manifest = renderPikaoaTestRuntimeManifest(target, context, { secretMode: "existing", initializerHook: "argo-presync" });
const manifest = renderPikaoaRuntimeManifest(target, context, { initializerHook: "argo-presync" });
return manifest.map((object) => Bun.YAML.stringify(object, null, 2).trim()).join("\n---\n") + "\n";
}
function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBinding): void {
function assertBinding(target: PikaoaRuntimeSpec, binding: PikaoaRendererBinding): void {
const expected: Readonly<Record<string, string>> = {
id: target.source.branch === "release" ? "pikaoa-nc01" : "pikaoa-test-nc01",
node: target.node,
lane: target.ci.lane,
namespace: target.ci.namespace,
@@ -349,7 +356,6 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin
serviceAccount: target.ci.serviceAccountName,
};
const actual: Readonly<Record<string, string | undefined>> = {
id: binding.consumer.id,
node: binding.consumer.node,
lane: binding.consumer.lane,
namespace: binding.consumer.namespace,
@@ -364,7 +370,7 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin
}
}
function assertSourceRepositoryContract(target: TestTargetSpec, sourceWorktree: string): void {
function assertSourceRepositoryContract(target: PikaoaRuntimeSpec, sourceWorktree: string): void {
for (const path of Object.values(target.build.dockerfiles)) {
if (!existsSync(resolve(sourceWorktree, path))) throw new Error(`PikaOA source worktree is missing ${path}`);
}
@@ -1,44 +1,28 @@
import type { TestTargetSpec } from "./pikaoa-test-target";
// SPEC: PJ2026-03 PikaOA v0.8.
import type { PikaoaRuntimeSpec } from "./pikaoa-runtime";
export type PikaoaTestTargetStep = "all" | "foundation" | "init" | "api" | "worker" | "web";
export interface PikaoaTestRenderContext {
export interface PikaoaRuntimeRenderContext {
instanceId: string;
commit: string;
step: PikaoaTestTargetStep;
namespace: string;
expiresAt: string;
apiImage: string;
workerImage: string;
webImage: string;
initializerImage: string;
}
export interface PikaoaTestSecretMaterial {
databaseURL: string;
sessionSecret: string;
adminToken: string;
administratorPassword: string;
employeePassword: string;
}
export interface PikaoaTestManifestOptions {
secretMode: "materialized" | "existing";
secrets?: PikaoaTestSecretMaterial;
export interface PikaoaRuntimeManifestOptions {
initializerHook?: "argo-presync";
}
const TARGET_LABEL = "pikaoa.unidesk.io/target";
const INSTANCE_LABEL = "pikaoa.unidesk.io/instance";
export function renderPikaoaTestRuntimeManifest(
target: TestTargetSpec,
context: PikaoaTestRenderContext,
options: PikaoaTestManifestOptions,
export function renderPikaoaRuntimeManifest(
target: PikaoaRuntimeSpec,
context: PikaoaRuntimeRenderContext,
options: PikaoaRuntimeManifestOptions,
): Record<string, unknown>[] {
if (options.secretMode === "materialized" && options.secrets === undefined) {
throw new Error("materialized PikaOA test manifest requires secret material");
}
const labels = ownershipLabels(target, context);
const apiSelector = componentSelector("api", context.instanceId);
const workerSelector = componentSelector("worker", context.instanceId);
@@ -60,10 +44,10 @@ export function renderPikaoaTestRuntimeManifest(
{
apiVersion: "v1", kind: "Namespace", metadata: { name: context.namespace, labels, annotations: namespaceAnnotations(target, context) },
},
...(options.secretMode === "materialized" ? [{
apiVersion: "v1", kind: "Secret", metadata: { name: target.runtime.secretName, namespace: context.namespace, labels }, type: "Opaque",
stringData: runtimeSecretData(target, context, options.secrets!),
}] : []),
{
apiVersion: "v1", kind: "ConfigMap", metadata: { name: target.runtime.configMapName, namespace: context.namespace, labels },
data: { "pikaoa.yaml": runtimeConfigData(target, context) },
},
{
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
spec: {
@@ -103,7 +87,7 @@ export function renderPikaoaTestRuntimeManifest(
],
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
}],
volumes: [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
volumes: [{ name: "runtime", configMap: { name: target.runtime.configMapName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
},
},
},
@@ -128,26 +112,13 @@ export function renderPikaoaTestRuntimeManifest(
];
}
export function pikaoaManifestForStep(manifest: Record<string, unknown>[], step: PikaoaTestTargetStep): Record<string, unknown>[] {
if (step === "all") return manifest;
return manifest.filter((object) => {
if (object.kind === "Namespace" || object.kind === "PersistentVolumeClaim") return true;
if (step === "foundation") return false;
if (object.kind === "Secret") return true;
if (step === "init") return object.kind === "Job";
const metadata = optionalRecord(object.metadata);
const labels = optionalRecord(metadata?.labels);
return labels?.["app.kubernetes.io/component"] === step;
});
}
export function ownershipLabels(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
export function ownershipLabels(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): Record<string, string> {
const release = target.source.branch === "release";
return {
"app.kubernetes.io/name": "pikaoa",
"app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-test-runtime",
"app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-development-runtime",
"app.kubernetes.io/managed-by": target.fieldManager,
[release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/test-runtime"]: "true",
[release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/development-runtime"]: "true",
[TARGET_LABEL]: target.id,
[INSTANCE_LABEL]: context.instanceId,
};
@@ -157,34 +128,17 @@ export function componentSelector(component: string, instanceId: string): Record
return { "app.kubernetes.io/name": "pikaoa", "app.kubernetes.io/component": component, [INSTANCE_LABEL]: instanceId };
}
export function namespaceAnnotations(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
export function namespaceAnnotations(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): Record<string, string> {
return {
"pikaoa.unidesk.io/source-repository": target.source.repository,
"pikaoa.unidesk.io/source-commit": context.commit,
"pikaoa.unidesk.io/expires-at": context.expiresAt,
"pikaoa.unidesk.io/ttl-seconds": String(target.ttlSeconds),
};
}
export function renderDatabaseURL(target: TestTargetSpec, sourceURL: string): string {
const databaseURL = new URL(sourceURL);
databaseURL.searchParams.set("search_path", target.database.schema);
return databaseURL.toString();
}
export function placeholderPikaoaSecrets(): PikaoaTestSecretMaterial {
return {
databaseURL: "postgresql://redacted@db.invalid/pikaoa_test?sslmode=require",
sessionSecret: "<redacted>",
adminToken: "<redacted>",
administratorPassword: "<redacted>",
employeePassword: "<redacted>",
"pikaoa.unidesk.io/delivery-managed": "true",
};
}
function deployment(
target: TestTargetSpec,
context: PikaoaTestRenderContext,
target: PikaoaRuntimeSpec,
context: PikaoaRuntimeRenderContext,
component: "api" | "worker" | "web",
image: string,
selector: Record<string, string>,
@@ -215,7 +169,7 @@ function deployment(
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
}],
volumes: [
...(component === "web" ? [] : [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
...(component === "web" ? [] : [{ name: "runtime", configMap: { name: target.runtime.configMapName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
...(component === "api" ? [{ name: target.runtime.attachment.claimName, persistentVolumeClaim: { claimName: target.runtime.attachment.claimName } }] : []),
],
},
@@ -224,15 +178,8 @@ function deployment(
};
}
function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderContext, secrets: PikaoaTestSecretMaterial): Record<string, string> {
const databaseURL = renderDatabaseURL(target, secrets.databaseURL);
return {
[target.database.connection.targetKey]: databaseURL,
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
[target.runtime.adminToken.targetKey]: secrets.adminToken,
[target.runtime.administrator.password.targetKey]: secrets.administratorPassword,
[target.runtime.employee.password.targetKey]: secrets.employeePassword,
"pikaoa.yaml": [
function runtimeConfigData(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): string {
return [
"api:",
` listen: 0.0.0.0:${target.runtime.apiPort}`,
"worker:",
@@ -243,21 +190,16 @@ function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderCont
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
` retryDelay: ${target.runtime.outboxRetryDelay}`,
"database:",
` url: ${JSON.stringify(databaseURL)}`,
' url: ""',
"identity:",
` sessionTTL: ${target.runtime.sessionTTL}`,
"attachment:",
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
` maxBytes: ${target.runtime.attachment.maxBytes}`,
"observability:",
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-test-${context.instanceId}`}`,
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-dev-${context.instanceId}`}`,
` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`,
`shutdownTimeout: ${target.runtime.shutdownTimeout}`,
"",
].join("\n"),
};
}
function optionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
].join("\n");
}
+505
View File
@@ -0,0 +1,505 @@
// SPEC: PJ2026-03 PikaOA v0.8.
import { resolveConfigRef } from "./ops/config-refs";
interface SecretSourceSpec {
purpose: string;
sourceRef: string;
sourceKey: string | null;
targetKey: string;
}
interface PrometheusSelectorSpec {
sourceRef: string;
key: string;
value: string;
presence: true;
fingerprint: string;
valuesPrinted: false;
}
export interface PikaoaRuntimeSpec {
id: string;
enabled: boolean;
node: string;
route: string;
namespace: string;
fieldManager: string;
source: {
mode: string;
repository: string;
commitPolicy: string;
branch: string;
worktreeRemote: string;
readUrl: string;
snapshotPrefix: string;
pullPolicy: string;
};
ci: {
lane: string;
namespace: string;
pipeline: string;
pipelineRunPrefix: string;
serviceAccountName: string;
workspaceSize: string;
pipelineTimeout: string;
toolsImage: string;
buildkitImage: string;
};
build: {
dockerfiles: { api: string; worker: string; web: string };
images: { api: string; worker: string; web: string };
networkMode: string;
};
gitops: {
readUrl: string;
writeUrl: string;
branch: string;
manifestPath: string;
credentialSecretName: string;
credentialTokenKey: string;
credentialUsername: string;
authorName: string;
authorEmail: string;
};
database: {
configRef: string;
name: string;
username: string;
schema: string;
connection: SecretSourceSpec;
};
runtime: {
replicas: { api: number; worker: number; web: number };
resources: Record<"api" | "worker" | "web" | "initializer", Record<string, unknown>>;
secretName: string;
configMapName: string;
apiServiceName: string;
apiPort: number;
workerMetricsPort: number;
webPort: number;
webApiUpstreamEnv: string;
attachment: {
storageRoot: string;
maxBytes: number;
claimName: string;
storageRequest: string;
storageClassName: string | null;
fsGroup: number;
accessModes: string[];
};
workerInterval: string;
outboxBatchSize: number;
outboxMaxAttempts: number;
outboxRetryDelay: string;
sessionTTL: string;
shutdownTimeout: string;
administrator: { username: string; password: SecretSourceSpec };
employee: { username: string; password: SecretSourceSpec };
sessionSecret: SecretSourceSpec;
adminToken: SecretSourceSpec;
};
initializer: {
mode: "fresh-database-only";
jobName: string;
backoffLimit: number;
command: string[];
};
exposure: {
serviceName: string;
serviceType: "NodePort";
hostIP: string;
port: number;
publicBaseUrl: string;
expectedA: string;
};
observability: {
otlpEndpoint: string;
prometheusScrape: boolean;
prometheusSelector: PrometheusSelectorSpec;
apiMetricsPath: string;
workerMetricsPath: string;
exporterWarning: boolean;
exporterBlocking: false;
};
probes: {
apiLivenessPath: string;
apiReadinessPath: string;
workerLivenessPath: string;
workerReadinessPath: string;
webLivenessPath: string;
webReadinessPath: string;
};
}
export function pikaoaDevelopmentRuntimeConfigRef(targetId: string): string {
simpleId(targetId, "PikaOA development runtime target id");
return `config/pikaoa.yaml#developmentRuntime.targets.${targetId}`;
}
export function resolvePikaoaDevelopmentRuntimeTargetByConfigRef(configRef: string): PikaoaRuntimeSpec {
return resolvePikaoaRuntimeTarget(configRef, "developmentRuntime");
}
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): PikaoaRuntimeSpec {
return resolvePikaoaRuntimeTarget(configRef, "releaseRuntime");
}
function resolvePikaoaRuntimeTarget(configRef: string, runtimeSection: "developmentRuntime" | "releaseRuntime"): PikaoaRuntimeSpec {
const resolved = resolveConfigRef(configRef, `PikaOA ${runtimeSection} configRef`);
const match = new RegExp(`^${runtimeSection}\\.targets\\.([A-Za-z0-9._-]+)$`, "u").exec(resolved.fragment);
if (resolved.file !== "config/pikaoa.yaml" || match === null) {
throw new Error(`PikaOA ${runtimeSection} configRef must select config/pikaoa.yaml#${runtimeSection}.targets.<id>`);
}
return parseRuntime(match[1]!, record(resolved.value, configRef), resolved.file, runtimeSection);
}
function parseRuntime(id: string, root: Record<string, unknown>, configLabel: string, runtimeSection: string): PikaoaRuntimeSpec {
const path = `${configLabel}.${runtimeSection}.targets.${id}`;
simpleId(id, "target id");
const source = record(root.source, `${path}.source`);
const ci = record(root.ci, `${path}.ci`);
const build = record(root.build, `${path}.build`);
const dockerfiles = record(build.dockerfiles, `${path}.build.dockerfiles`);
const buildImages = record(build.images, `${path}.build.images`);
const gitops = record(root.gitops, `${path}.gitops`);
const gitopsCredential = record(gitops.credential, `${path}.gitops.credential`);
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`);
const employee = record(runtime.employee, `${path}.runtime.employee`);
const initializer = record(root.initializer, `${path}.initializer`);
const exposure = record(root.exposure, `${path}.exposure`);
const observability = record(root.observability, `${path}.observability`);
const prometheus = record(observability.prometheus, `${path}.observability.prometheus`);
const exporterFailure = record(observability.exporterFailure, `${path}.observability.exporterFailure`);
const probes = record(root.probes, `${path}.probes`);
const apiProbes = record(probes.api, `${path}.probes.api`);
const workerProbes = record(probes.worker, `${path}.probes.worker`);
const webProbes = record(probes.web, `${path}.probes.web`);
if (exporterFailure.blocking !== false || exporterFailure.warning !== true) {
throw new Error(`${path}.observability.exporterFailure 必须为 warning=true、blocking=false`);
}
const target: PikaoaRuntimeSpec = {
id,
enabled: bool(root.enabled, `${path}.enabled`),
node: text(root.node, `${path}.node`),
route: text(root.route, `${path}.route`),
namespace: kubernetesName(root.namespace, `${path}.namespace`),
fieldManager: kubernetesName(root.fieldManager, `${path}.fieldManager`),
source: {
mode: exact(source.mode, `${path}.source.mode`, "pac-source-build"),
repository: text(source.repository, `${path}.source.repository`),
commitPolicy: exact(source.commitPolicy, `${path}.source.commitPolicy`, "pac-revision-required"),
branch: text(source.branch, `${path}.source.branch`),
worktreeRemote: text(source.worktreeRemote, `${path}.source.worktreeRemote`),
readUrl: text(source.readUrl, `${path}.source.readUrl`),
snapshotPrefix: text(source.snapshotPrefix, `${path}.source.snapshotPrefix`),
pullPolicy: oneOf(source.pullPolicy, `${path}.source.pullPolicy`, ["Always", "IfNotPresent", "Never"]),
},
ci: {
lane: simpleId(text(ci.lane, `${path}.ci.lane`), `${path}.ci.lane`),
namespace: kubernetesName(ci.namespace, `${path}.ci.namespace`),
pipeline: kubernetesName(ci.pipeline, `${path}.ci.pipeline`),
pipelineRunPrefix: kubernetesName(ci.pipelineRunPrefix, `${path}.ci.pipelineRunPrefix`),
serviceAccountName: kubernetesName(ci.serviceAccountName, `${path}.ci.serviceAccountName`),
workspaceSize: text(ci.workspaceSize, `${path}.ci.workspaceSize`),
pipelineTimeout: text(ci.pipelineTimeout, `${path}.ci.pipelineTimeout`),
toolsImage: imageReference(ci.toolsImage, `${path}.ci.toolsImage`),
buildkitImage: imageReference(ci.buildkitImage, `${path}.ci.buildkitImage`),
},
build: {
dockerfiles: {
api: relativePath(dockerfiles.api, `${path}.build.dockerfiles.api`),
worker: relativePath(dockerfiles.worker, `${path}.build.dockerfiles.worker`),
web: relativePath(dockerfiles.web, `${path}.build.dockerfiles.web`),
},
images: {
api: imageRepository(buildImages.api, `${path}.build.images.api`),
worker: imageRepository(buildImages.worker, `${path}.build.images.worker`),
web: imageRepository(buildImages.web, `${path}.build.images.web`),
},
networkMode: exact(build.networkMode, `${path}.build.networkMode`, "host"),
},
gitops: {
readUrl: text(gitops.readUrl, `${path}.gitops.readUrl`),
writeUrl: text(gitops.writeUrl, `${path}.gitops.writeUrl`),
branch: text(gitops.branch, `${path}.gitops.branch`),
manifestPath: relativePath(gitops.manifestPath, `${path}.gitops.manifestPath`),
credentialSecretName: kubernetesName(gitopsCredential.secretName, `${path}.gitops.credential.secretName`),
credentialTokenKey: text(gitopsCredential.tokenKey, `${path}.gitops.credential.tokenKey`),
credentialUsername: text(gitopsCredential.username, `${path}.gitops.credential.username`),
authorName: text(gitopsAuthor.name, `${path}.gitops.author.name`),
authorEmail: text(gitopsAuthor.email, `${path}.gitops.author.email`),
},
database: {
configRef: text(database.configRef, `${path}.database.configRef`),
name: postgresIdentifier(database.name, `${path}.database.name`),
username: postgresIdentifier(database.username, `${path}.database.username`),
schema: postgresIdentifier(database.schema, `${path}.database.schema`),
connection: secretSource(database.connection, "database.connection", `${path}.database.connection`, true),
},
runtime: {
replicas: {
api: positiveInteger(replicas.api, `${path}.runtime.replicas.api`, Number.MAX_SAFE_INTEGER),
worker: positiveInteger(replicas.worker, `${path}.runtime.replicas.worker`, Number.MAX_SAFE_INTEGER),
web: positiveInteger(replicas.web, `${path}.runtime.replicas.web`, Number.MAX_SAFE_INTEGER),
},
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(runtime.secretName, `${path}.runtime.secretName`),
configMapName: kubernetesName(runtime.configMapName, `${path}.runtime.configMapName`),
apiServiceName: kubernetesName(runtime.apiServiceName, `${path}.runtime.apiServiceName`),
apiPort: positiveInteger(runtime.apiPort, `${path}.runtime.apiPort`, 65_535),
workerMetricsPort: positiveInteger(runtime.workerMetricsPort, `${path}.runtime.workerMetricsPort`, 65_535),
webPort: positiveInteger(runtime.webPort, `${path}.runtime.webPort`, 65_535),
webApiUpstreamEnv: envName(runtime.webApiUpstreamEnv, `${path}.runtime.webApiUpstreamEnv`),
attachment: {
storageRoot: absolutePath(attachment.storageRoot, `${path}.runtime.attachment.storageRoot`),
maxBytes: positiveInteger(attachment.maxBytes, `${path}.runtime.attachment.maxBytes`, Number.MAX_SAFE_INTEGER),
claimName: kubernetesName(attachment.claimName, `${path}.runtime.attachment.claimName`),
storageRequest: text(attachment.storageRequest, `${path}.runtime.attachment.storageRequest`),
storageClassName: nullableText(attachment.storageClassName, `${path}.runtime.attachment.storageClassName`),
fsGroup: positiveInteger(attachment.fsGroup, `${path}.runtime.attachment.fsGroup`, 2_147_483_647),
accessModes: stringList(attachment.accessModes, `${path}.runtime.attachment.accessModes`, ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]),
},
workerInterval: duration(runtime.workerInterval, `${path}.runtime.workerInterval`),
outboxBatchSize: positiveInteger(outbox.batchSize, `${path}.runtime.outbox.batchSize`, Number.MAX_SAFE_INTEGER),
outboxMaxAttempts: positiveInteger(outbox.maxAttempts, `${path}.runtime.outbox.maxAttempts`, Number.MAX_SAFE_INTEGER),
outboxRetryDelay: duration(outbox.retryDelay, `${path}.runtime.outbox.retryDelay`),
sessionTTL: duration(runtime.sessionTTL, `${path}.runtime.sessionTTL`),
shutdownTimeout: duration(runtime.shutdownTimeout, `${path}.runtime.shutdownTimeout`),
administrator: {
username: text(administrator.username, `${path}.runtime.administrator.username`),
password: secretSource(administrator.password, "runtime.administrator.password", `${path}.runtime.administrator.password`),
},
employee: {
username: text(employee.username, `${path}.runtime.employee.username`),
password: secretSource(employee.password, "runtime.employee.password", `${path}.runtime.employee.password`),
},
sessionSecret: secretSource(runtime.sessionSecret, "runtime.sessionSecret", `${path}.runtime.sessionSecret`),
adminToken: secretSource(runtime.adminToken, "runtime.adminToken", `${path}.runtime.adminToken`, true),
},
initializer: {
mode: exact(initializer.mode, `${path}.initializer.mode`, "fresh-database-only") as "fresh-database-only",
jobName: kubernetesName(initializer.jobName, `${path}.initializer.jobName`),
backoffLimit: nonNegativeInteger(initializer.backoffLimit, `${path}.initializer.backoffLimit`),
command: nonEmptyStringList(initializer.command, `${path}.initializer.command`),
},
exposure: {
serviceName: kubernetesName(exposure.serviceName, `${path}.exposure.serviceName`),
serviceType: exact(exposure.serviceType, `${path}.exposure.serviceType`, "NodePort") as "NodePort",
hostIP: ipv4Address(exposure.hostIP, `${path}.exposure.hostIP`),
port: positiveInteger(exposure.port, `${path}.exposure.port`, 65_535),
publicBaseUrl: httpsUrl(exposure.publicBaseUrl, `${path}.exposure.publicBaseUrl`),
expectedA: ipv4Address(exposure.expectedA, `${path}.exposure.expectedA`),
},
observability: {
otlpEndpoint: otlpGrpcEndpoint(observability.otlpEndpoint, `${path}.observability.otlpEndpoint`),
prometheusScrape: bool(prometheus.scrape, `${path}.observability.prometheus.scrape`),
prometheusSelector: prometheusSelector(prometheus.configRef, `${path}.observability.prometheus`),
apiMetricsPath: httpPath(prometheus.apiPath, `${path}.observability.prometheus.apiPath`),
workerMetricsPath: httpPath(prometheus.workerPath, `${path}.observability.prometheus.workerPath`),
exporterWarning: true,
exporterBlocking: false,
},
probes: {
apiLivenessPath: httpPath(apiProbes.livenessPath, `${path}.probes.api.livenessPath`),
apiReadinessPath: httpPath(apiProbes.readinessPath, `${path}.probes.api.readinessPath`),
workerLivenessPath: httpPath(workerProbes.livenessPath, `${path}.probes.worker.livenessPath`),
workerReadinessPath: httpPath(workerProbes.readinessPath, `${path}.probes.worker.readinessPath`),
webLivenessPath: httpPath(webProbes.livenessPath, `${path}.probes.web.livenessPath`),
webReadinessPath: httpPath(webProbes.readinessPath, `${path}.probes.web.readinessPath`),
},
};
const secretKeys = [target.database.connection, target.runtime.sessionSecret, target.runtime.adminToken, target.runtime.administrator.password, target.runtime.employee.password]
.map((item) => item.targetKey);
if (new Set(secretKeys).size !== secretKeys.length || secretKeys.includes("pikaoa.yaml")) {
throw new Error(`${path} 的 Secret targetKey 必须唯一且不能使用 pikaoa.yaml`);
}
return target;
}
function secretSource(value: unknown, purpose: string, path: string, requireSourceKey = false): SecretSourceSpec {
const source = record(value, path);
const targetKey = envName(source.targetKey, `${path}.targetKey`);
const sourceKey = nullableText(source.sourceKey, `${path}.sourceKey`);
if (requireSourceKey && sourceKey === null) throw new Error(`${path}.sourceKey 必须显式声明`);
if (sourceKey !== null) envName(sourceKey, `${path}.sourceKey`);
return { purpose, sourceRef: text(source.sourceRef, `${path}.sourceRef`), sourceKey, targetKey };
}
function prometheusSelector(value: unknown, path: string): PrometheusSelectorSpec {
const sourceRef = text(value, `${path}.configRef`);
const resolved = resolveConfigRef(sourceRef, `${path}.configRef`);
const selector = record(resolved.value, sourceRef);
return {
sourceRef,
key: kubernetesLabelKey(selector.key, `${path}.configRef.key`),
value: kubernetesLabelValue(selector.value, `${path}.configRef.value`),
presence: true,
fingerprint: `sha256:${resolved.sha256}`,
valuesPrinted: false,
};
}
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} 必须是 YAML object`);
return value as Record<string, unknown>;
}
function text(value: unknown, path: string): string {
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} 必须是非空字符串`);
return value.trim();
}
function nullableText(value: unknown, path: string): string | null {
return value === undefined || value === null ? null : text(value, path);
}
function bool(value: unknown, path: string): boolean {
if (typeof value !== "boolean") throw new Error(`${path} 必须是 boolean`);
return value;
}
function positiveInteger(value: unknown, path: string, max: number): number {
if (!Number.isInteger(value) || Number(value) <= 0 || Number(value) > max) throw new Error(`${path} 必须是 1-${max} 的整数`);
return Number(value);
}
function nonNegativeInteger(value: unknown, path: string): number {
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new Error(`${path} 必须是非负安全整数`);
return Number(value);
}
function simpleId(value: string, path: string): string {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} ID`);
return value;
}
function kubernetesName(value: unknown, path: string): string {
const result = text(value, path);
if (result.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(result)) throw new Error(`${path} Kubernetes DNS label`);
return result;
}
function postgresIdentifier(value: unknown, path: string): string {
const result = text(value, path);
if (!/^[a-z_][a-z0-9_]{0,62}$/u.test(result)) throw new Error(`${path} PostgreSQL identifier`);
return result;
}
function imageReference(value: unknown, path: string): string {
const result = text(value, path);
if (/\s/u.test(result) || !result.includes(":")) throw new Error(`${path} tag/template `);
return result;
}
function imageRepository(value: unknown, path: string): string {
const result = text(value, path);
if (/\s/u.test(result) || result.includes("@") || /:[^/]+$/u.test(result)) throw new Error(`${path} tag/digest `);
return result;
}
function relativePath(value: unknown, path: string): string {
const result = text(value, path);
if (result.startsWith("/") || result.split(/[\\/]/u).includes("..")) throw new Error(`${path} `);
return result;
}
function absolutePath(value: unknown, path: string): string {
const result = text(value, path);
if (!result.startsWith("/") || result.includes("\0")) throw new Error(`${path} 必须是绝对路径`);
return result;
}
function envName(value: unknown, path: string): string {
const result = text(value, path);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(result)) throw new Error(`${path} `);
return result;
}
function duration(value: unknown, path: string): string {
const result = text(value, path);
if (!/^[1-9][0-9]*(?:ms|s|m|h)$/u.test(result)) throw new Error(`${path} duration`);
return result;
}
function httpPath(value: unknown, path: string): string {
const result = text(value, path);
if (!result.startsWith("/") || /[\s?#]/u.test(result)) throw new Error(`${path} HTTP path`);
return result;
}
function httpsUrl(value: unknown, path: string): string {
const result = text(value, path);
const parsed = new URL(result);
if (parsed.protocol !== "https:") throw new Error(`${path} 必须是 HTTPS URL`);
return result;
}
function ipv4Address(value: unknown, path: string): string {
const result = text(value, path);
const octets = result.split(".");
if (octets.length !== 4 || octets.some((item) => !/^(?:0|[1-9][0-9]{0,2})$/u.test(item) || Number(item) > 255)) throw new Error(`${path} IPv4 `);
return result;
}
function otlpGrpcEndpoint(value: unknown, path: string): string {
const result = text(value, path);
if (result.includes("://") || !/^[A-Za-z0-9.-]+:[1-9][0-9]{0,4}$/u.test(result)) throw new Error(`${path} OTLP gRPC host:port`);
const port = Number(result.slice(result.lastIndexOf(":") + 1));
if (port > 65_535) throw new Error(`${path} 端口必须是 1-65535`);
return result;
}
function kubernetesLabelKey(value: unknown, path: string): string {
const result = text(value, path);
const parts = result.split("/");
const name = parts.pop() ?? "";
const prefix = parts.length === 1 ? parts[0] : null;
const namePattern = /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u;
const prefixPattern = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/u;
if (parts.length > 1 || !namePattern.test(name) || (prefix !== null && !prefixPattern.test(prefix))) throw new Error(`${path} 必须是 Kubernetes label key`);
return result;
}
function kubernetesLabelValue(value: unknown, path: string): string {
const result = text(value, path);
if (!/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(result)) throw new Error(`${path} Kubernetes label value`);
return result;
}
function nonEmptyStringList(value: unknown, path: string): string[] {
if (!Array.isArray(value) || value.length === 0) throw new Error(`${path} 必须是非空字符串数组`);
return value.map((item, index) => text(item, `${path}[${index}]`));
}
function stringList<T extends string>(value: unknown, path: string, allowed: readonly T[]): T[] {
const items = nonEmptyStringList(value, path).map((item) => oneOf(item, path, allowed));
if (new Set(items).size !== items.length) throw new Error(`${path} 不能包含重复值`);
return items;
}
function oneOf<T extends string>(value: unknown, path: string, allowed: readonly T[]): T {
if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`${path} 必须是 ${allowed.join("、")}`);
return value as T;
}
function exact(value: unknown, path: string, expected: string): string {
if (value !== expected) throw new Error(`${path} 必须是 ${expected}`);
return expected;
}
@@ -1,487 +0,0 @@
import assert from "node:assert/strict";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "bun:test";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { runPikaoaCommand } from "./pikaoa-test-target";
import type { SshCaptureResult } from "./ssh";
test("PikaOA test target submits once and exposes bounded typed status", async () => {
const root = mkdtempSync(join(tmpdir(), "pikaoa-test-target-"));
const stateRoot = join(root, "state");
const fakeRoot = join(root, "fake-k8s");
const binDir = join(root, "bin");
const secretsDir = join(root, "secrets");
mkdirSync(binDir, { recursive: true });
mkdirSync(secretsDir, { recursive: true });
mkdirSync(join(fakeRoot, "namespaces"), { recursive: true });
const secretValues = ["fixture-db-password", "fixture-admin-password", "fixture-employee-password", "fixture-session-secret", "fixture-oa-admin-token"];
for (const [name, value] of [
["pikaoa-test.env", `DATABASE_URL=postgresql://pikaoa_test:${secretValues[0]}@db.example.invalid:5432/pikaoa_test?sslmode=require\nPIKAOA_SESSION_SECRET=${secretValues[3]}\nOA_ADMIN_TOKEN=${secretValues[4]}`],
["pikaoa-test-admin-password.txt", secretValues[1]],
["pikaoa-test-employee-password.txt", secretValues[2]],
]) writeFileSync(join(secretsDir, name), `${value}\n`, { mode: 0o600 });
const fakeKubectl = join(binDir, "kubectl");
writeFileSync(fakeKubectl, `#!/bin/sh
set -u
root="$FAKE_K8S_ROOT"
args="$*"
namespace=""
previous=""
for argument in "$@"; do
if [ "$previous" = "-n" ]; then namespace="$argument"; fi
previous="$argument"
done
namespace_after_resource() {
previous=""
for argument in "$@"; do
if [ "$previous" = "namespace" ]; then printf '%s' "$argument"; return; fi
previous="$argument"
done
}
if printf '%s' "$args" | grep -Eq '(^| )apply( |$)'; then
file=""
previous=""
for argument in "$@"; do
if [ "$previous" = "-f" ]; then file="$argument"; fi
previous="$argument"
done
if grep -q '"kind":"Namespace"' "$file"; then
applied_identity="$(python3 - "$file" <<'PY'
import json, pathlib, sys
for line in pathlib.Path(sys.argv[1]).read_text().splitlines():
if not line or line == "---":
continue
item = json.loads(line)
if item.get("kind") == "Namespace":
print(item["metadata"]["name"] + "|" + item["metadata"]["labels"]["pikaoa.unidesk.io/instance"])
break
PY
)"
applied_namespace="\${applied_identity%%|*}"
applied_instance="\${applied_identity##*|}"
printf '%s' "$applied_instance" >"$root/namespaces/$applied_namespace"
fi
[ ! -f "$root/slow" ] || sleep 0.2
exit 0
fi
if printf '%s' "$args" | grep -q ' rollout status '; then
[ ! -f "$root/slow" ] || sleep 0.4
exit 0
fi
if printf '%s' "$args" | grep -q ' get job/pikaoa-init '; then
if [ -f "$root/fail-initializer-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi
if [ -f "$root/failure-target-initializer" ]; then
printf '%s\n' '{"status":{"conditions":[{"type":"FailureTarget","status":"True","reason":"BackoffLimitExceeded"}]}}'
elif [ -f "$root/fail-initializer" ]; then
printf '%s\n' '{"status":{"conditions":[{"type":"Failed","status":"True","reason":"BackoffLimitExceeded"}]}}'
else
printf '%s\n' '{"status":{"conditions":[{"type":"Complete","status":"True"}]}}'
fi
exit 0
fi
if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then
name="$(namespace_after_resource "$@")"
if [ -f "$root/fail-runtime-query" ]; then
printf '%s\n' 'unable to connect to the server' >&2
exit 1
fi
if [ ! -f "$root/namespaces/$name" ]; then
printf 'Error from server (NotFound): namespaces "%s" not found\n' "$name" >&2
exit 1
fi
instance="$(cat "$root/namespaces/$name")"
if printf '%s' "$args" | grep -q 'managed-by'; then printf '%s' 'unidesk-pikaoa-test-target'; exit 0; fi
if printf '%s' "$args" | grep -q 'unidesk.*target'; then printf '%s' 'TEST01'; exit 0; fi
if printf '%s' "$args" | grep -q 'unidesk.*instance'; then printf '%s' "$instance"; exit 0; fi
printf '{"metadata":{"name":"%s","labels":{"app.kubernetes.io/managed-by":"unidesk-pikaoa-test-target","pikaoa.unidesk.io/target":"TEST01","pikaoa.unidesk.io/instance":"%s"}}}\n' "$name" "$instance"
exit 0
fi
if printf '%s' "$args" | grep -q ' get deployment,job,service,persistentvolumeclaim,pod '; then
if [ -f "$root/fail-runtime-resource-query" ]; then
printf '%s\n' 'resource query failed' >&2
exit 1
fi
printf '%s\n' '{"items":[]}'
exit 0
fi
if printf '%s' "$args" | grep -Eq '(^| )delete namespace '; then
name="$(namespace_after_resource "$@")"
rm -f "$root/namespaces/$name"
exit 0
fi
printf 'unsupported fake kubectl call: %s\n' "$args" >&2
exit 64
`, { mode: 0o700 });
chmodSync(fakeKubectl, 0o700);
const fixturePath = join(root, "pikaoa.yaml");
const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8")
.replace("validationOnly: true", "validationOnly: false")
.replace("statusRequestTimeoutSeconds: 3", "statusRequestTimeoutSeconds: 31")
.replace("logTailLines: 8", "logTailLines: 101")
.replace("/tmp/unidesk-pikaoa-test-target-fixture", stateRoot);
writeFileSync(fixturePath, fixture);
const config = {} as UniDeskConfig;
let lastCapture: SshCaptureResult | null = null;
const localCapture = async (_config: UniDeskConfig, _route: string, args: string[], stdin: string): Promise<SshCaptureResult> => {
assert.deepEqual(args, ["sh"]);
const child = Bun.spawn(["sh"], {
env: { ...process.env, PATH: `${binDir}:${process.env.PATH ?? ""}`, FAKE_K8S_ROOT: fakeRoot },
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
child.stdin.write(stdin);
child.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
lastCapture = { stdout, stderr, exitCode };
return lastCapture;
};
const args = (action: string, instance: string, withCommit = false, step = "all"): string[] => [
"test-target", action, "--config", fixturePath, "--target", "TEST01", "--instance", instance,
...(withCommit ? ["--commit", "0123456789abcdef"] : []),
"--step", step,
...((action === "start" || action === "stop") ? ["--confirm"] : []),
"--output", "json",
];
const projection = async (action: string, instance: string, withCommit = false, step = "all"): Promise<Record<string, unknown>> => {
const result = await runPikaoaCommand(config, args(action, instance, withCommit, step), localCapture);
return JSON.parse(result.renderedText) as Record<string, unknown>;
};
const projectionWithCommit = async (action: string, instance: string, commit: string, step = "all"): Promise<Record<string, unknown>> => {
const commandArgs = args(action, instance, true, step);
commandArgs[commandArgs.indexOf("--commit") + 1] = commit;
const result = await runPikaoaCommand(config, commandArgs, localCapture);
return JSON.parse(result.renderedText) as Record<string, unknown>;
};
const waitFor = async (instance: string, expectedCode: string, step = "all"): Promise<Record<string, unknown>> => {
let lastStatus: Record<string, unknown> | null = null;
for (let attempt = 0; attempt < 80; attempt += 1) {
const value = await projection("status", instance, false, step);
const status = value.status as Record<string, unknown>;
if (status.code === expectedCode) return value;
lastStatus = status;
await Bun.sleep(25);
}
assert.fail(`status did not reach ${expectedCode}: ${JSON.stringify(lastStatus)}`);
};
try {
const planned = await projection("plan", "runtime", true);
const plan = planned.plan as Record<string, unknown>;
const secret = plan.secret as Record<string, unknown>;
const sources = secret.sources as Array<Record<string, unknown>>;
assert.equal(sources.find((source) => source.purpose === "database.connection")?.presence, true);
assert.equal(sources.find((source) => source.purpose === "runtime.adminToken")?.targetKey, "OA_ADMIN_TOKEN");
assert.equal(sources.find((source) => source.purpose === "runtime.adminToken")?.valuesPrinted, false);
const objects = plan.objects as Array<Record<string, unknown>>;
const observability = plan.observability as Record<string, unknown>;
const prometheus = observability.prometheus as Record<string, unknown>;
const selector = prometheus.selector as Record<string, unknown>;
assert.equal(objects.some((object) => object.kind === "StatefulSet" || object.name === "pikaoa-postgres"), false);
assert.equal(objects.some((object) => object.kind === "Deployment" && object.name === "pikaoa-web"), true);
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-api")?.podFsGroup, 65532);
assert.equal(observability.otlpEndpoint, "otel-collector.observability.svc.cluster.local:4317");
assert.equal(selector.sourceRef, "config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector");
assert.equal(selector.presence, true);
assert.match(String(selector.fingerprint), /^sha256:[0-9a-f]{64}$/u);
assert.equal(selector.valuesPrinted, false);
for (const component of ["pikaoa-api", "pikaoa-worker"]) {
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === component)?.prometheusSelectorLabel, selector.value);
}
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-web")?.prometheusSelectorLabel, undefined);
assert.deepEqual(plan.exposure, { serviceType: "NodePort", serviceName: "pikaoa-web", hostIP: "192.0.2.10", port: 32080 });
assert.deepEqual(plan.webApiUpstream, { envName: "PIKAOA_API_UPSTREAM", value: "http://pikaoa-api:8080", scope: "same-namespace" });
const missing = await projection("status", "runtime", false, "api");
assert.equal((missing.status as Record<string, unknown>).code, "start-not-found");
writeFileSync(join(fakeRoot, "slow"), "1\n");
const submitted = await projection("start", "runtime", true);
assert.equal(submitted.code, "start-submitted", JSON.stringify(lastCapture));
assert.equal(submitted.mutation, true);
assert.deepEqual((submitted.target as Record<string, unknown>).taskState, {
rootPath: stateRoot,
statusRequestTimeoutSeconds: 31,
logTailLines: 101,
});
const running = await projection("status", "runtime");
assert.equal((running.status as Record<string, unknown>).code, "start-running");
const concurrentCommit = "fedcba9876543210";
const concurrent = await projectionWithCommit("start", "runtime", concurrentCommit);
assert.equal(concurrent.code, "start-instance-conflict");
assert.equal(concurrent.mutation, false);
assert.equal(concurrent.previousTaskReset, false);
rmSync(join(fakeRoot, "slow"));
const succeeded = await waitFor("runtime", "start-succeeded");
const succeededStatus = succeeded.status as Record<string, unknown>;
assert.equal((succeededStatus.task as Record<string, unknown>).exitCode, 0);
assert.ok((succeededStatus.logTail as unknown[]).length <= 101);
writeFileSync(join(fakeRoot, "fail-runtime-query"), "1\n");
const runtimeQueryFailed = await projection("status", "runtime");
const runtimeQueryFailedStatus = runtimeQueryFailed.status as Record<string, unknown>;
assert.equal(runtimeQueryFailed.ok, false);
assert.equal(runtimeQueryFailedStatus.code, "runtime-query-failed");
assert.equal(runtimeQueryFailedStatus.taskCode, "start-succeeded");
assert.equal((runtimeQueryFailedStatus.runtime as Record<string, unknown>).code, "runtime-query-failed");
const runtimeQueryFailedText = await runPikaoaCommand(config, args("status", "runtime").slice(0, -2), localCapture);
assert.match(runtimeQueryFailedText.renderedText, /status=runtime-query-failed/);
rmSync(join(fakeRoot, "fail-runtime-query"));
writeFileSync(join(fakeRoot, "fail-runtime-resource-query"), "1\n");
const runtimeResourceQueryFailed = await projection("status", "runtime");
const runtimeResourceQueryFailedStatus = runtimeResourceQueryFailed.status as Record<string, unknown>;
assert.equal(runtimeResourceQueryFailed.ok, false);
assert.equal(runtimeResourceQueryFailedStatus.code, "runtime-resource-query-failed");
assert.equal(runtimeResourceQueryFailedStatus.taskCode, "start-succeeded");
assert.equal((runtimeResourceQueryFailedStatus.runtime as Record<string, unknown>).code, "runtime-resource-query-failed");
rmSync(join(fakeRoot, "fail-runtime-resource-query"));
const repeated = await projection("start", "runtime", true);
assert.equal(repeated.code, "start-already-succeeded");
assert.equal(repeated.mutation, false);
const rerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit);
assert.equal(rerunSubmitted.code, "start-submitted");
assert.equal(rerunSubmitted.previousTaskReset, true);
const rerunSucceeded = await waitFor("runtime", "start-succeeded");
assert.equal(((rerunSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).sourceCommit, concurrentCommit);
writeFileSync(fixturePath, readFileSync(fixturePath, "utf8").replace(" livenessPath: /healthz\n", " livenessPath: /healthz-v2\n"));
const manifestRerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit);
assert.equal(manifestRerunSubmitted.code, "start-submitted");
assert.equal(manifestRerunSubmitted.previousTaskReset, true);
const manifestRerunSucceeded = await waitFor("runtime", "start-succeeded");
await Bun.sleep(5);
const sameRequestAfterRenderTimeChanged = await projectionWithCommit("start", "runtime", concurrentCommit);
assert.equal(sameRequestAfterRenderTimeChanged.code, "start-already-succeeded");
assert.equal(sameRequestAfterRenderTimeChanged.previousTaskReset, false);
writeFileSync(join(fakeRoot, "slow"), "1\n");
const workerMissingSubmitted = await projection("start", "runtime", true, "api");
const workerPid = Number((workerMissingSubmitted.remote as Record<string, unknown>).pid);
assert.equal(Number.isSafeInteger(workerPid) && workerPid > 0, true);
process.kill(workerPid, "SIGKILL");
await Bun.sleep(25);
const workerMissingReset = await projectionWithCommit("start", "runtime", concurrentCommit, "api");
assert.equal(workerMissingReset.code, "start-submitted");
assert.equal(workerMissingReset.previousTaskReset, true);
rmSync(join(fakeRoot, "slow"));
const workerMissingSucceeded = await waitFor("runtime", "start-succeeded", "api");
writeFileSync(join(fakeRoot, "fail-initializer"), "1\n");
await projection("start", "runtime", true, "init");
const failed = await waitFor("runtime", "start-failed", "init");
const failedTask = ((failed.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(failedTask.code, "initializer-job-failed");
assert.equal(failedTask.exitCode, 46);
rmSync(join(fakeRoot, "fail-initializer"));
writeFileSync(join(fakeRoot, "failure-target-initializer"), "1\n");
await projectionWithCommit("start", "runtime", concurrentCommit, "init");
const failureTarget = await waitFor("runtime", "start-failed", "init");
const failureTargetTask = ((failureTarget.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(failureTargetTask.code, "initializer-job-failure-target");
assert.equal(failureTargetTask.exitCode, 46);
rmSync(join(fakeRoot, "failure-target-initializer"));
writeFileSync(join(fakeRoot, "fail-initializer-query"), "1\n");
await projectionWithCommit("start", "runtime", "abcdef0123456789", "init");
const initializerQueryFailed = await waitFor("runtime", "start-failed", "init");
const initializerQueryFailedTask = ((initializerQueryFailed.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(initializerQueryFailedTask.code, "initializer-job-query-failed");
assert.equal(initializerQueryFailedTask.exitCode, 46);
rmSync(join(fakeRoot, "fail-initializer-query"));
await projectionWithCommit("start", "runtime", "1234567890abcdef", "init");
const initializerSucceeded = await waitFor("runtime", "start-succeeded", "init");
assert.equal((((initializerSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).exitCode), 0);
writeFileSync(join(fakeRoot, "slow"), "1\n");
await projection("start", "runtime", true, "web");
const stopped = await projection("stop", "runtime", false, "web");
assert.equal(stopped.startTaskDisposition, "cancel-requested");
assert.equal(stopped.mutation, true);
await waitFor("runtime", "start-canceled", "web");
for (const name of [
"pikaoa-test.env",
"pikaoa-test-admin-password.txt",
"pikaoa-test-employee-password.txt",
]) rmSync(join(secretsDir, name));
const foundationPlan = await projection("plan", "foundation", false, "foundation");
assert.equal(foundationPlan.readyForMutation, true);
assert.deepEqual(foundationPlan.missingFields, []);
assert.deepEqual((foundationPlan.blockers as unknown[]), []);
const foundationObjects = ((foundationPlan.plan as Record<string, unknown>).objects as Array<Record<string, unknown>>);
assert.deepEqual(foundationObjects.map((object) => object.kind), ["Namespace", "PersistentVolumeClaim"]);
assert.equal(((foundationPlan.plan as Record<string, unknown>).secret as Record<string, unknown>).requiredForStep, false);
const foundationSubmitted = await projection("start", "foundation", false, "foundation");
assert.equal(foundationSubmitted.code, "start-submitted", JSON.stringify(lastCapture));
const foundationSucceeded = await waitFor("foundation", "start-succeeded", "foundation");
const foundationRepeated = await projection("start", "foundation", false, "foundation");
assert.equal(foundationRepeated.code, "start-already-succeeded");
const foundationRerun = await projectionWithCommit("start", "foundation", concurrentCommit, "foundation");
assert.equal(foundationRerun.code, "start-submitted");
assert.equal(foundationRerun.previousTaskReset, true);
const foundationRerunSucceeded = await waitFor("foundation", "start-succeeded", "foundation");
const serialized = JSON.stringify({
submitted,
succeeded,
concurrent,
rerunSubmitted,
rerunSucceeded,
manifestRerunSubmitted,
manifestRerunSucceeded,
sameRequestAfterRenderTimeChanged,
workerMissingSubmitted,
workerMissingReset,
workerMissingSucceeded,
failed,
failureTarget,
initializerQueryFailed,
initializerSucceeded,
stopped,
foundationSubmitted,
foundationSucceeded,
foundationRepeated,
foundationRerun,
foundationRerunSucceeded,
});
for (const value of secretValues) assert.equal(serialized.includes(value), false);
assert.equal(serialized.includes("postgresql://pikaoa_test:"), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
}, 30_000);
test("validation-only fixture never invokes remote capture", async () => {
let calls = 0;
const forbiddenCapture = async (): Promise<SshCaptureResult> => {
calls += 1;
throw new Error("validation-only fixture attempted remote capture");
};
const result = await runPikaoaCommand({} as UniDeskConfig, [
"test-target", "status", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--output", "json",
], forbiddenCapture);
assert.equal(calls, 0);
const payload = JSON.parse(result.renderedText) as Record<string, unknown>;
assert.equal(payload.remoteQueried, false);
assert.equal((payload.status as Record<string, unknown>).remoteQueried, false);
const planResult = await runPikaoaCommand({} as UniDeskConfig, [
"test-target", "plan", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--commit", "0123456789abcdef", "--output", "json",
], forbiddenCapture);
const planPayload = JSON.parse(planResult.renderedText) as Record<string, unknown>;
const plan = planPayload.plan as Record<string, unknown>;
assert.deepEqual(plan.database, {
configRef: "config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url",
name: "pikaoa_test",
username: "pikaoa_test",
schema: "pikaoa",
connection: {
sourceRef: "./secrets/pikaoa-test.env",
sourceKey: "DATABASE_URL",
targetKey: "DATABASE_URL",
valuesPrinted: false,
},
dsnParameterPresence: { sslmode: true, search_path: true },
valuesPrinted: false,
});
assert.deepEqual(plan.runtimeConfig, {
secretName: "pikaoa-test-runtime",
key: "pikaoa.yaml",
mountPath: "/etc/pikaoa/pikaoa.yaml",
consumers: ["initializer", "api", "worker"],
valuesPrinted: false,
});
assert.equal(planPayload.valuesPrinted, false);
assert.equal(((planPayload.observability as Record<string, unknown>).prometheus as Record<string, unknown>).selector !== undefined, true);
assert.equal(JSON.stringify(planPayload).includes("fixture-db-password"), false);
assert.equal(JSON.stringify(planPayload).includes("postgres://"), false);
const missingSchemaRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-schema-"));
const missingSchemaPath = join(missingSchemaRoot, "pikaoa.yaml");
const missingSchemaFixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8")
.replace(" schema: pikaoa\n", "");
writeFileSync(missingSchemaPath, missingSchemaFixture);
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, [
"test-target", "status", "--config", missingSchemaPath, "--target", "TEST01", "--instance", "missing-schema", "--output", "json",
], forbiddenCapture),
/testRuntime\.targets\.TEST01\.database\.schema /u,
);
assert.equal(calls, 0);
} finally {
rmSync(missingSchemaRoot, { recursive: true, force: true });
}
const missingSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-"));
const missingSelectorPath = join(missingSelectorRoot, "pikaoa.yaml");
const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8");
writeFileSync(missingSelectorPath, fixture.replace(" configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector\n", ""));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorPath, "--target", "TEST01", "--instance", "missing-selector", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef /u,
);
} finally {
rmSync(missingSelectorRoot, { recursive: true, force: true });
}
const invalidSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-invalid-selector-"));
const invalidSelectorPath = join(invalidSelectorRoot, "pikaoa.yaml");
writeFileSync(invalidSelectorPath, fixture.replace(
"config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector",
"config/fixtures/pikaoa-observability-invalid-label.yaml#metricsBackend.discovery.labelSelector",
));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", invalidSelectorPath, "--target", "TEST01", "--instance", "invalid-selector", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef\.key Kubernetes label key/u,
);
} finally {
rmSync(invalidSelectorRoot, { recursive: true, force: true });
}
const missingSelectorPathRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-path-"));
const missingSelectorConfigPath = join(missingSelectorPathRoot, "pikaoa.yaml");
writeFileSync(missingSelectorConfigPath, fixture.replace("metricsBackend.discovery.labelSelector", "metricsBackend.discovery.missingSelector"));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorConfigPath, "--target", "TEST01", "--instance", "missing-selector-path", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef .*missingSelector/u,
);
} finally {
rmSync(missingSelectorPathRoot, { recursive: true, force: true });
}
const owningConfig = Bun.YAML.parse(readFileSync(rootPath("config", "pikaoa.yaml"), "utf8")) as Record<string, unknown>;
const testTarget = (((owningConfig.testRuntime as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
assert.equal((testTarget.observability as Record<string, unknown>).otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
const production = (((owningConfig.delivery as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
const productionObservability = (production.runtime as Record<string, unknown>).observability as Record<string, unknown>;
assert.equal(productionObservability.otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
assert.equal("otlpHttpEndpoint" in productionObservability, false);
const unconfiguredRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-unconfigured-"));
const unconfiguredPath = join(unconfiguredRoot, "pikaoa.yaml");
writeFileSync(unconfiguredPath, "version: 1\nkind: pikaoa-platform-delivery\ntestRuntime:\n defaultTargetId: null\n targets: {}\n");
try {
const unconfigured = await runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", unconfiguredPath, "--output", "json"], forbiddenCapture);
const unconfiguredPayload = JSON.parse(unconfigured.renderedText) as Record<string, unknown>;
assert.equal(calls, 0);
assert.equal(unconfiguredPayload.configured, false);
assert.equal(unconfiguredPayload.remoteQueried, false);
} finally {
rmSync(unconfiguredRoot, { recursive: true, force: true });
}
});
File diff suppressed because it is too large Load Diff
@@ -49,18 +49,18 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
test("keeps PikaOA master and release authorities on distinct Secret keys", () => {
const config = readGiteaConfig();
const testAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-test-nc01");
const developmentAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-dev-nc01");
const productionAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-nc01");
const testKey = testAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
const developmentKey = developmentAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
const productionKey = productionAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
expect(testAuthority?.upstream.branch).toBe("master");
expect(developmentAuthority?.upstream.branch).toBe("master");
expect(productionAuthority?.upstream.branch).toBe("release");
expect(testKey).toBe("github-token-pikainc-pikaoa");
expect(developmentKey).toBe("github-token-pikainc-pikaoa");
expect(productionKey).toBe("github-token-pikainc-pikaoa-release");
expect(productionKey).not.toBe(testKey);
expect(productionKey).not.toBe(developmentKey);
expect(githubTokenEnvNameForSecretKey(productionKey ?? ""))
.not.toBe(githubTokenEnvNameForSecretKey(testKey ?? ""));
.not.toBe(githubTokenEnvNameForSecretKey(developmentKey ?? ""));
});
test("normalizes Secret keys to bounded environment variable names", () => {
@@ -74,7 +74,15 @@ test("versioned admission desired state reports controller proof and candidate d
toState: "started",
}]);
expect(contract.child.enabled).toBe(false);
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01", "selfmedia-production-nc01", "pikaoa-test-nc01"]);
expect(contract.consumers.map((item) => item.id)).toEqual([
"agentrun-nc01-v02",
"agentrun-nc01-release",
"platform-infra-sub2rank-nc01",
"selfmedia-nc01",
"selfmedia-production-nc01",
"pikaoa-dev-nc01",
"pikaoa-nc01",
]);
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
const policy = items[0];
@@ -159,19 +159,19 @@ test("unselected parse and repository errors remain warnings while selected obje
expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");
const invalidRepositoryDocument = pacConfigDocument();
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-test-nc01");
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-dev-nc01");
if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
expect(isolated.validationWarnings).toContainEqual({
code: "pac-unselected-repository-invalid",
object: { kind: "repository", id: "pikaoa-test-nc01" },
object: { kind: "repository", id: "pikaoa-dev-nc01" },
consumer: null,
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-test-nc01",
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-dev-nc01",
blocking: false,
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(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-dev-nc01")).toBe(false);
expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");
const duplicateSelectedRepository = pacConfigDocument();
@@ -99,11 +99,11 @@ function runtimeInput(commit: string | null = sourceCommit): Record<string, unkn
}
describe("PaC source artifact CLI contract", () => {
test("accepts PikaOA test runtime provenance", () => {
test("accepts PikaOA development runtime provenance", () => {
const expected = {
configRef: "config/pikaoa.yaml#testRuntime.targets.NC01",
configRef: "config/pikaoa.yaml#developmentRuntime.targets.NC01",
effectiveConfigSha256: `sha256:${"a".repeat(64)}`,
renderer: "pikaoa-test-runtime",
renderer: "pikaoa-development-runtime",
mode: "embedded-pipeline-spec",
} as const;
const manifest = {
@@ -113,9 +113,9 @@ describe("PaC source artifact CLI contract", () => {
});
test("uses PikaOA-specific PipelineRun labels", () => {
const binding = { consumer: { id: "pikaoa-test-nc01", node: "NC01" } } as Parameters<typeof pipelineRunLabels>[0];
const binding = { consumer: { id: "pikaoa-dev-nc01", node: "NC01" } } as Parameters<typeof pipelineRunLabels>[0];
expect(pipelineRunLabels(binding, "pikaoa")).toEqual({
"app.kubernetes.io/name": "pikaoa-test-nc01-pac",
"app.kubernetes.io/name": "pikaoa-dev-nc01-pac",
"app.kubernetes.io/part-of": "pikaoa",
"unidesk.ai/source-commit": "{{ revision }}",
"pikaoa.unidesk.io/source-commit": "{{ revision }}",
@@ -125,7 +125,7 @@ describe("PaC source artifact CLI contract", () => {
test("maps the PikaOA shared workspace to the declared RWO PVC size", () => {
const binding = {
consumer: { sourceArtifact: { renderer: "pikaoa-test-runtime" } },
consumer: { sourceArtifact: { renderer: "pikaoa-development-runtime" } },
repository: { params: { workspace_pvc_size: "8Gi" } },
} as Parameters<typeof pipelineRunWorkspaces>[0];
expect(pipelineRunWorkspaces(binding, { workspaces: [{ name: "workspace" }] })).toEqual([{
@@ -1,3 +1,4 @@
// SPEC: PJ2026-03 PikaOA v0.8.
import { createHash } from "node:crypto";
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -19,10 +20,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, pikaoaReleaseSourceWorktreeRemote, pikaoaTestSourceWorktreeRemote } from "./pikaoa-test-delivery-renderer";
import { renderPikaoaDesiredPipeline, pikaoaDevelopmentSourceWorktreeRemote, pikaoaReleaseSourceWorktreeRemote } from "./pikaoa-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" | "pikaoa-release-runtime";
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-development-runtime" | "pikaoa-release-runtime";
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
@@ -502,7 +503,7 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
? renderSub2RankDesiredPipeline(binding)
: sourceArtifact.renderer === "selfmedia-runtime"
? renderSelfMediaPipeline(binding, sourceWorktree)
: renderPikaoaTestPipeline(binding, sourceWorktree);
: renderPikaoaPipeline(binding, sourceWorktree);
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
? rendered.pipeline
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
@@ -586,8 +587,8 @@ function renderSelfMediaPipeline(binding: PacSourceArtifactBinding, sourceWorktr
};
}
function renderPikaoaTestPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
const rendered = renderPikaoaTestDesiredPipeline(binding, sourceWorktree);
function renderPikaoaPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
const rendered = renderPikaoaDesiredPipeline(binding, sourceWorktree);
return {
pipeline: rendered.pipeline,
provenance: {
@@ -675,7 +676,7 @@ function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Rec
? "sub2rank"
: provenance.renderer === "selfmedia-runtime"
? "selfmedia"
: provenance.renderer === "pikaoa-test-runtime"
: provenance.renderer === "pikaoa-development-runtime" || provenance.renderer === "pikaoa-release-runtime"
? "pikaoa"
: "agentrun",
),
@@ -857,7 +858,7 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
}
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" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) {
if ((binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-development-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)) {
@@ -875,7 +876,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" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) {
if (name === "source" || (name === "workspace" && (binding.consumer.sourceArtifact.renderer === "pikaoa-development-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) {
return {
name,
volumeClaimTemplate: {
@@ -953,7 +954,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-development-runtime") return pikaoaDevelopmentSourceWorktreeRemote(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;
@@ -1074,7 +1075,7 @@ export function pacSourceArtifactProvenanceFromManifest(manifest: Record<string,
&& provenance.renderer !== "hwlab-runtime-lane"
&& provenance.renderer !== "sub2rank-platform-service"
&& provenance.renderer !== "selfmedia-runtime"
&& provenance.renderer !== "pikaoa-test-runtime"
&& provenance.renderer !== "pikaoa-development-runtime"
&& provenance.renderer !== "pikaoa-release-runtime") return null;
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
return provenance as PacSourceArtifactProvenance;
@@ -1,3 +1,4 @@
// SPEC: PJ2026-03 PikaOA v0.8.
import { randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
@@ -869,7 +870,7 @@ function parseConsumerDeliveryProvenance(value: Record<string, unknown>, path: s
function parseSourceArtifact(value: Record<string, unknown>, 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", "pikaoa-release-runtime"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-development-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);
@@ -879,7 +880,7 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
if (renderer === "hwlab-runtime-lane" && mode !== "remote-pipeline-annotation") throw new Error(`${path}.renderer hwlab-runtime-lane requires remote-pipeline-annotation`);
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-development-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-development-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,
@@ -1221,7 +1222,7 @@ function validateConsumerConfig(
if (consumer.sourceArtifact?.renderer === "sub2rank-platform-service" && consumer.runnerServiceAccount === null) {
throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for sub2rank-platform-service`);
}
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime" || consumer.sourceArtifact?.renderer === "pikaoa-test-runtime") {
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime" || consumer.sourceArtifact?.renderer === "pikaoa-development-runtime") {
if (consumer.runnerServiceAccount === null) throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for private GitOps runtime renderers`);
if (consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined) {
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private repository`);