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