From 344be2cd53995294a19bdc18498a66a3b7b81e0b Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 23:55:43 +0000 Subject: [PATCH] feat: migrate jd01 hwlab ci to gitea pac --- config/cicd-branch-followers.yaml | 10 +- config/hwlab-node-control-plane.yaml | 9 +- config/hwlab-node-lanes.yaml | 13 ++- config/platform-infra/gitea.yaml | 22 ++++ config/platform-infra/pipelines-as-code.yaml | 36 +++++- scripts/src/hwlab-node-control-plane-model.ts | 7 +- scripts/src/hwlab-node-control-plane.ts | 14 ++- scripts/src/hwlab-node-lanes.ts | 23 ++-- scripts/src/hwlab-node/cleanup.ts | 47 ++++++++ scripts/src/hwlab-node/git-mirror.ts | 107 +++++++++++++++--- scripts/src/hwlab-node/status.ts | 106 +++++++++++++++++ scripts/src/platform-infra-gitea.ts | 9 +- .../src/platform-infra-pipelines-as-code.ts | 8 +- 13 files changed, 365 insertions(+), 46 deletions(-) diff --git a/config/cicd-branch-followers.yaml b/config/cicd-branch-followers.yaml index 06637ce9..e2e90e19 100644 --- a/config/cicd-branch-followers.yaml +++ b/config/cicd-branch-followers.yaml @@ -59,16 +59,16 @@ controller: followers: - id: hwlab-jd01-v03 - enabled: true + enabled: false adapter: hwlab-node-runtime - description: Follow HWLAB v0.3 into the JD01 HWLAB node runtime. + description: Retired for GH-1557; HWLAB v0.3 JD01 runtime CI is driven by Gitea mirror -> Pipelines-as-Code -> Tekton -> ArgoCD. source: repository: pikasTech/HWLAB branch: v0.3 branchRef: config/hwlab-node-lanes.yaml#lanes.v03.sourceBranch - authorityRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.git.readUrl - snapshotPrefix: refs/unidesk/snapshots/hwlab-node-runtime/v0.3 - snapshotRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.git.readUrl + authorityRef: config/platform-infra/gitea.yaml#sourceAuthority.repositories.hwlab-jd01-v03 + snapshotPrefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3 + snapshotRef: config/platform-infra/gitea.yaml#sourceAuthority.repositories.hwlab-jd01-v03.snapshot.prefix target: node: JD01 lane: v03 diff --git a/config/hwlab-node-control-plane.yaml b/config/hwlab-node-control-plane.yaml index cbd5d04d..16d91cdb 100644 --- a/config/hwlab-node-control-plane.yaml +++ b/config/hwlab-node-control-plane.yaml @@ -481,15 +481,16 @@ targets: repository: pikasTech/HWLAB branch: v0.3 sourceAuthority: - mode: gitMirrorSnapshot - resolver: k8s-git-mirror + mode: giteaSnapshot + resolver: gitea-mirror + giteaMirrorRepoKey: hwlab-jd01-v03 allowHostGit: false allowHostWorkspace: false allowGithubDirectInPipeline: false sourceSnapshot: - stageRefPrefix: refs/unidesk/snapshots/hwlab-node-runtime/{branch} + stageRefPrefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/{branch} missingObjectPolicy: fail-fast - refreshPolicy: sync-before-snapshot + refreshPolicy: gitea-controlled-snapshot gitops: branch: v0.3-gitops path: deploy/gitops/node/jd01/runtime-v03 diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 96c76235..a60910db 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -880,6 +880,17 @@ lanes: JD01: node: JD01 workspace: /root/workspace/hwlab-v03 + sourceAuthority: + mode: giteaSnapshot + resolver: gitea-mirror + giteaMirrorRepoKey: hwlab-jd01-v03 + allowHostGit: false + allowHostWorkspace: false + allowGithubDirectInPipeline: false + sourceSnapshot: + stageRefPrefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/{branch} + missingObjectPolicy: fail-fast + refreshPolicy: gitea-controlled-snapshot sourceWorkspace: git: remoteName: github @@ -956,7 +967,7 @@ lanes: controlPlaneFieldManager: unidesk-hwlab-jd01-v03-control-plane git: url: git@github.com:pikasTech/HWLAB.git - readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git + readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git argo: repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git diff --git a/config/platform-infra/gitea.yaml b/config/platform-infra/gitea.yaml index 768d01ba..df1a99ed 100644 --- a/config/platform-infra/gitea.yaml +++ b/config/platform-infra/gitea.yaml @@ -9,6 +9,7 @@ metadata: - 1548 - 1549 - 1550 + - 1557 defaults: targetId: JD01 @@ -116,6 +117,27 @@ sourceAuthority: readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git configRef: config/cicd-branch-followers.yaml#controller.source.gitMirrorReadUrl disposition: replaced-by-gitea + - key: hwlab-jd01-v03 + targetId: JD01 + upstream: + repository: pikasTech/HWLAB + cloneUrl: https://github.com/pikasTech/HWLAB.git + branch: v0.3 + gitea: + owner: mirrors + name: pikasTech-HWLAB + mirrorMode: controlled-push + publicRead: true + readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git + gitops: + branch: v0.3-gitops + flushDisposition: retained-for-gitops-flush + snapshot: + prefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3 + legacyGitMirror: + readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git + configRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.git.readUrl + disposition: replaced-by-gitea targets: - id: JD01 diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index d286ac36..2c0843e4 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -8,6 +8,7 @@ metadata: relatedIssues: - 1552 - 1555 + - 1557 defaults: targetId: JD01 @@ -43,7 +44,7 @@ gitea: secretSourceRef: .env/pipelines-as-code.webhook events: - push - branch: v0.2 + branch: params.source_branch repositories: - id: agentrun-jd01-v02 @@ -89,6 +90,30 @@ repositories: sentinel_id: jd01-web-probe-sentinel pipeline_name: hwlab-web-probe-sentinel-jd01-pac pipeline_run_prefix: hwlab-web-probe-sentinel-jd01 + - id: hwlab-jd01-v03 + name: hwlab-jd01-v03 + namespace: hwlab-ci + providerType: gitea + url: https://gitea.pikapython.com/mirrors/pikasTech-HWLAB + cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git + owner: mirrors + repo: pikasTech-HWLAB + secretName: pac-gitea-hwlab-jd01-v03 + tokenKey: token + webhookSecretKey: webhook.secret + concurrencyLimit: 1 + params: + git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git + git_write_url: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git + source_branch: v0.3 + gitops_branch: v0.3-gitops + source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3 + pipeline_name: hwlab-jd01-v03-ci-image-publish + pipeline_run_prefix: hwlab-jd01-v03-ci-poll + service_account: hwlab-jd01-v03-tekton-runner + node: JD01 + lane: v03 + runtime_namespace: hwlab-v03 consumers: - id: agentrun-jd01-v02 @@ -109,3 +134,12 @@ consumers: pipelineRunPrefix: hwlab-web-probe-sentinel-jd01 argoNamespace: argocd argoApplication: hwlab-web-probe-sentinel-jd01 + - id: hwlab-jd01-v03 + repositoryRef: hwlab-jd01-v03 + node: JD01 + lane: v03 + namespace: hwlab-ci + pipeline: hwlab-jd01-v03-ci-image-publish + pipelineRunPrefix: hwlab-jd01-v03-ci-poll + argoNamespace: argocd + argoApplication: hwlab-node-v03 diff --git a/scripts/src/hwlab-node-control-plane-model.ts b/scripts/src/hwlab-node-control-plane-model.ts index 7902dd05..e441c189 100644 --- a/scripts/src/hwlab-node-control-plane-model.ts +++ b/scripts/src/hwlab-node-control-plane-model.ts @@ -298,8 +298,9 @@ export interface ControlPlaneTargetSpec { repository: string; branch: string; sourceAuthority: { - mode: "gitMirrorSnapshot"; - resolver: "k8s-git-mirror"; + mode: "gitMirrorSnapshot" | "giteaSnapshot"; + resolver: "k8s-git-mirror" | "gitea-mirror"; + giteaMirrorRepoKey?: string; allowHostGit: false; allowHostWorkspace: false; allowGithubDirectInPipeline: false; @@ -307,7 +308,7 @@ export interface ControlPlaneTargetSpec { sourceSnapshot: { stageRefPrefix: string; missingObjectPolicy: "fail-fast"; - refreshPolicy: "sync-before-snapshot"; + refreshPolicy: "sync-before-snapshot" | "gitea-controlled-snapshot"; }; }; gitops: { branch: string; path: string }; diff --git a/scripts/src/hwlab-node-control-plane.ts b/scripts/src/hwlab-node-control-plane.ts index ca6249dc..ed68026a 100644 --- a/scripts/src/hwlab-node-control-plane.ts +++ b/scripts/src/hwlab-node-control-plane.ts @@ -1954,11 +1954,17 @@ function targetSpec(raw: Record, index: number): ControlPlaneTa function controlPlaneSourceAuthoritySpec(raw: Record, path: string): ControlPlaneTargetSpec["source"]["sourceAuthority"] { const mode = stringField(raw, "mode", path); const resolver = stringField(raw, "resolver", path); - if (mode !== "gitMirrorSnapshot") throw new Error(`${path}.mode must be gitMirrorSnapshot`); - if (resolver !== "k8s-git-mirror") throw new Error(`${path}.resolver must be k8s-git-mirror`); + if (mode !== "gitMirrorSnapshot" && mode !== "giteaSnapshot") throw new Error(`${path}.mode must be gitMirrorSnapshot or giteaSnapshot`); + if (resolver !== "k8s-git-mirror" && resolver !== "gitea-mirror") throw new Error(`${path}.resolver must be k8s-git-mirror or gitea-mirror`); + if (mode === "gitMirrorSnapshot" && resolver !== "k8s-git-mirror") throw new Error(`${path}.resolver must be k8s-git-mirror when mode is gitMirrorSnapshot`); + if (mode === "giteaSnapshot" && resolver !== "gitea-mirror") throw new Error(`${path}.resolver must be gitea-mirror when mode is giteaSnapshot`); + const giteaMirrorRepoKey = optionalStringField(raw, "giteaMirrorRepoKey", path); + if (mode === "giteaSnapshot" && giteaMirrorRepoKey === undefined) throw new Error(`${path}.giteaMirrorRepoKey is required when mode is giteaSnapshot`); + if (giteaMirrorRepoKey !== undefined && !/^[A-Za-z0-9._-]+$/u.test(giteaMirrorRepoKey)) throw new Error(`${path}.giteaMirrorRepoKey must be a simple Gitea mirror repo key`); return { mode, resolver, + ...(giteaMirrorRepoKey === undefined ? {} : { giteaMirrorRepoKey }), allowHostGit: falseBooleanField(raw, "allowHostGit", path), allowHostWorkspace: falseBooleanField(raw, "allowHostWorkspace", path), allowGithubDirectInPipeline: falseBooleanField(raw, "allowGithubDirectInPipeline", path), @@ -1973,7 +1979,7 @@ function controlPlaneSourceSnapshotSpec(raw: Record, path: stri const missingObjectPolicy = stringField(raw, "missingObjectPolicy", path); const refreshPolicy = stringField(raw, "refreshPolicy", path); if (missingObjectPolicy !== "fail-fast") throw new Error(`${path}.missingObjectPolicy must be fail-fast`); - if (refreshPolicy !== "sync-before-snapshot") throw new Error(`${path}.refreshPolicy must be sync-before-snapshot`); + if (refreshPolicy !== "sync-before-snapshot" && refreshPolicy !== "gitea-controlled-snapshot") throw new Error(`${path}.refreshPolicy must be sync-before-snapshot or gitea-controlled-snapshot`); return { stageRefPrefix, missingObjectPolicy, refreshPolicy }; } @@ -2126,7 +2132,7 @@ function tektonArgoObserverRole(target: ControlPlaneTargetSpec, labels: Record } { + if (spec.sourceAuthority?.mode === "giteaSnapshot") return resolveNodeRuntimeGiteaLaneHead(spec); const mirror = nodeRuntimeSourceMirrorTarget(spec); const script = [ "set +e", @@ -134,6 +135,52 @@ export function resolveNodeRuntimeLaneHead(spec: HwlabRuntimeLaneSpec): { source return { sourceCommit: result.exitCode === 0 ? match : null, result, stdoutRecovery: payloadResolution.diagnostics }; } +function resolveNodeRuntimeGiteaLaneHead(spec: HwlabRuntimeLaneSpec): { sourceCommit: string | null; result: CommandResult; stdoutRecovery?: Record } { + const repoKey = nodeRuntimeGiteaRepoKey(spec); + const result = runCommand([ + "bun", + "scripts/cli.ts", + "platform-infra", + "gitea", + "mirror", + "status", + "--target", + spec.nodeId, + "--repo", + repoKey, + "--raw", + ], repoRoot, { timeoutMs: 60_000 }); + const payloadResolution = resolveRuntimeCleanupJson(result, "platform-infra gitea mirror status JSON"); + const payload = runtimeRecord(payloadResolution.parsed?.data ?? payloadResolution.parsed); + const repositories = Array.isArray(payload.repositories) + ? payload.repositories.filter((item): item is Record => typeof item === "object" && item !== null && !Array.isArray(item)) + : []; + const repo = repositories[0] ?? {}; + const branchCommit = typeof repo.branchCommit === "string" && /^[0-9a-f]{40}$/iu.test(repo.branchCommit) ? repo.branchCommit.toLowerCase() : null; + const snapshotCommit = typeof repo.snapshotCommit === "string" && /^[0-9a-f]{40}$/iu.test(repo.snapshotCommit) ? repo.snapshotCommit.toLowerCase() : null; + const snapshotRef = typeof repo.snapshotRef === "string" ? repo.snapshotRef : null; + const sourceCommit = branchCommit !== null && snapshotCommit === branchCommit ? branchCommit : null; + return { + sourceCommit: result.exitCode === 0 ? sourceCommit : null, + result, + stdoutRecovery: { + ...payloadResolution.diagnostics, + sourceAuthority: "gitea-snapshot", + repoKey, + branchCommit, + snapshotCommit, + snapshotRef, + snapshotPresent: sourceCommit !== null, + }, + }; +} + +function nodeRuntimeGiteaRepoKey(spec: HwlabRuntimeLaneSpec): string { + const repoKey = spec.sourceAuthority?.giteaMirrorRepoKey; + if (typeof repoKey !== "string" || repoKey.length === 0) throw new Error(`giteaMirrorRepoKey is required for node=${spec.nodeId} lane=${spec.lane} gitea source authority`); + return repoKey; +} + function resolveRuntimeCleanupJson(result: CommandResult, requestedStdoutType: string): { parsed: Record | null; diagnostics: Record } { const resolved = resolveCliChildJsonCommandResult({ result, diff --git a/scripts/src/hwlab-node/git-mirror.ts b/scripts/src/hwlab-node/git-mirror.ts index 18767102..d6385b1b 100644 --- a/scripts/src/hwlab-node/git-mirror.ts +++ b/scripts/src/hwlab-node/git-mirror.ts @@ -35,7 +35,7 @@ import { parseNodeScopedDelegatedOptions } from "./plan"; import { compactNodeRuntimeTaskRunDiagnostic, nodeRuntimeControlPlaneStatus, nodeRuntimePipelineFailureSummary } from "./render"; import { compactRuntimeCommand } from "./runtime-common"; import { compactNodeRuntimeGitMirrorObservation, compactNodeRuntimeGitMirrorRun, nodeRuntimeEnsureGitMirrorFlushed, nodeRuntimeEnsureGitMirrorSourceCurrent, nodeRuntimeExternalPostgresSecretRows, nodeRuntimeGitMirrorRun, nodeRuntimeGitMirrorStatus, nodeRuntimeOpportunisticGitMirrorFlush, nodeRuntimeOpportunisticGitMirrorSync, nodeScopedFullOutput, type NodeRuntimeGitMirrorRunOptions } from "./status"; -import { record } from "./utils"; +import { parseJsonObject, record, statusText } from "./utils"; import { webObserveTable } from "./web-observe-render"; import { createNodeRuntimePipelineRun, getNodeRuntimePipelineRun, nodeRuntimePipelineRunManifest, printNodeRuntimeTriggerProgress, waitForNodeRuntimePipelineRunTerminal } from "./web-probe"; import { webObserveShort, webObserveText } from "./web-probe-observe"; @@ -53,20 +53,7 @@ export function nodeRuntimeTriggerCurrent(scoped: ReturnType Math.max(0, Math.floor((triggerDeadlineMs - Date.now()) / 1000)); const triggerGitMirrorOptions = nodeRuntimeTriggerGitMirrorOptions(triggerDeadlineMs); - const sourceSyncScoped = nodeRuntimeScopedForTriggerDeadline(scoped, triggerDeadlineMs); - const sourceSnapshotSync = scoped.dryRun - ? null - : sourceSyncScoped === null - ? nodeRuntimeTriggerBudgetExhausted(scoped, "source-snapshot-sync", "-", "-") - : nodeRuntimeGitMirrorRun({ - ...sourceSyncScoped, - domain: "git-mirror", - action: "sync", - confirm: true, - dryRun: false, - wait: true, - discardStaleGitops: scoped.discardStaleGitops === true || scoped.rerun === true, - }, triggerGitMirrorOptions); + const sourceSnapshotSync = scoped.dryRun ? null : nodeRuntimeSourceSnapshotSync(scoped, triggerDeadlineMs, triggerGitMirrorOptions); if (sourceSnapshotSync !== null && sourceSnapshotSync.ok !== true) { return { ok: false, @@ -184,7 +171,9 @@ export function nodeRuntimeTriggerCurrent(scoped: ReturnType, + deadlineMs: number, + gitMirrorOptions: NodeRuntimeGitMirrorRunOptions, +): Record { + const syncScoped = nodeRuntimeScopedForTriggerDeadline(scoped, deadlineMs); + if (syncScoped === null) return nodeRuntimeTriggerBudgetExhausted(scoped, "source-snapshot-sync", "-", "-"); + if (scoped.spec.sourceAuthority?.mode === "giteaSnapshot") return nodeRuntimeGiteaMirrorSync(scoped, deadlineMs); + return nodeRuntimeGitMirrorRun({ + ...syncScoped, + domain: "git-mirror", + action: "sync", + confirm: true, + dryRun: false, + wait: true, + discardStaleGitops: scoped.discardStaleGitops === true || scoped.rerun === true, + }, gitMirrorOptions); +} + +function nodeRuntimeGiteaMirrorSync(scoped: ReturnType, deadlineMs: number): Record { + const repoKey = scoped.spec.sourceAuthority?.giteaMirrorRepoKey; + if (typeof repoKey !== "string" || repoKey.length === 0) { + return { + ok: false, + action: "platform-infra-gitea-mirror-sync", + mode: "gitea-controlled-snapshot-sync", + mutation: false, + degradedReason: "node-runtime-gitea-repo-key-missing", + }; + } + const timeoutMs = Math.max(1_000, Math.min(120_000, deadlineMs - Date.now())); + const result = runCommand([ + "bun", + "scripts/cli.ts", + "platform-infra", + "gitea", + "mirror", + "sync", + "--target", + scoped.node, + "--repo", + repoKey, + "--confirm", + "--raw", + ], repoRoot, { timeoutMs }); + const parsedRoot = record(parseJsonObject(statusText(result))); + const parsed = record(parsedRoot.data ?? parsedRoot); + const ok = result.exitCode === 0 && (parsed.ok === true || parsedRoot.ok === true); + return { + ok, + action: "platform-infra-gitea-mirror-sync", + mode: "gitea-controlled-snapshot-sync", + mutation: ok, + repoKey, + sourceAuthority: "gitea-snapshot", + sourceBundles: parsed.sourceBundles ?? null, + repositories: parsed.repositories ?? null, + result: compactRuntimeCommand(result), + remote: parsed, + degradedReason: ok ? undefined : "node-runtime-gitea-source-snapshot-sync-failed", + next: ok ? undefined : { + status: `bun scripts/cli.ts platform-infra gitea mirror status --target ${scoped.node} --repo ${repoKey}`, + retry: `bun scripts/cli.ts platform-infra gitea mirror sync --target ${scoped.node} --repo ${repoKey} --confirm`, + }, + }; +} + +function nodeRuntimeEnsureGiteaSourceCurrent( + scoped: ReturnType, + sourceCommit: string, + pipelineRun: string, + options: NodeRuntimeGitMirrorRunOptions, +): Record { + const flush = nodeRuntimeEnsureGitMirrorFlushed(scoped, "pre", sourceCommit, pipelineRun, null, options); + return { + ok: flush.ok === true, + mode: "gitea-source-authority", + sourceAuthority: "gitea-snapshot", + sourceCommit, + sourceCurrent: true, + legacyGitMirrorRole: "gitops-flush-only", + flush, + degradedReason: flush.ok === true ? undefined : "node-runtime-git-mirror-pre-flush-failed", + }; +} + function nodeRuntimeScopedForTriggerDeadline( scoped: ReturnType, deadlineMs: number, diff --git a/scripts/src/hwlab-node/status.ts b/scripts/src/hwlab-node/status.ts index 7328a2f4..c24362d2 100644 --- a/scripts/src/hwlab-node/status.ts +++ b/scripts/src/hwlab-node/status.ts @@ -61,6 +61,111 @@ export function nodeRuntimeExternalPostgresSecretRows(secrets: Record): Record { + if (scoped.spec.sourceAuthority?.mode === "giteaSnapshot") return nodeRuntimeGiteaSourceStatus(scoped); + return nodeRuntimeLegacyGitMirrorStatus(scoped); +} + +function nodeRuntimeGiteaSourceStatus(scoped: ReturnType): Record { + const repoKey = scoped.spec.sourceAuthority?.giteaMirrorRepoKey; + if (typeof repoKey !== "string" || repoKey.length === 0) { + return { + ok: false, + command: `hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`, + node: scoped.node, + lane: scoped.lane, + mode: "gitea-source+legacy-gitops-flush", + mutation: false, + degradedReason: "node-runtime-gitea-repo-key-missing", + }; + } + const legacy = nodeRuntimeLegacyGitMirrorStatus(scoped); + const result = runCommand([ + "bun", + "scripts/cli.ts", + "platform-infra", + "gitea", + "mirror", + "status", + "--target", + scoped.node, + "--repo", + repoKey, + "--raw", + ], repoRoot, { timeoutMs: scoped.timeoutSeconds * 1000 }); + const parsedRoot = record(parseJsonObject(statusText(result))); + const parsed = record(parsedRoot.data ?? parsedRoot); + const repositories = Array.isArray(parsed.repositories) + ? parsed.repositories.filter((item): item is Record => typeof item === "object" && item !== null && !Array.isArray(item)) + : []; + const repo = repositories[0] ?? {}; + const branchCommit = typeof repo.branchCommit === "string" && /^[0-9a-f]{40}$/iu.test(repo.branchCommit) ? repo.branchCommit.toLowerCase() : null; + const snapshotCommit = typeof repo.snapshotCommit === "string" && /^[0-9a-f]{40}$/iu.test(repo.snapshotCommit) ? repo.snapshotCommit.toLowerCase() : null; + const snapshotRef = typeof repo.snapshotRef === "string" ? repo.snapshotRef : null; + const sourceReady = branchCommit !== null && snapshotCommit === branchCommit; + const legacySummary = compactNodeRuntimeGitMirrorStatus(legacy); + const localGitops = typeof legacySummary.localGitops === "string" ? legacySummary.localGitops : null; + const githubGitops = typeof legacySummary.githubGitops === "string" ? legacySummary.githubGitops : null; + const pendingFlush = legacySummary.pendingFlush === true || (localGitops !== null && githubGitops !== null && localGitops !== githubGitops); + const summary = { + localSource: branchCommit, + githubSource: branchCommit, + sourceAuthority: "gitea-snapshot", + sourceStageRef: snapshotRef, + sourceSnapshot: snapshotCommit, + localGitops, + githubGitops, + pendingFlush, + flushNeeded: pendingFlush, + githubInSync: sourceReady && localGitops !== null && githubGitops !== null && localGitops === githubGitops, + refSources: { + localSource: `gitea:${repoKey}:refs/heads/${scoped.spec.sourceBranch}`, + githubSource: `gitea:${repoKey}:refs/heads/${scoped.spec.sourceBranch}`, + sourceSnapshot: snapshotRef, + localGitops: "legacy-git-mirror:refs/heads/" + scoped.spec.gitopsBranch, + githubGitops: "legacy-git-mirror:refs/mirror-stage/heads/" + scoped.spec.gitopsBranch, + githubFieldsAreMirrorStageCache: false, + }, + }; + const ok = result.exitCode === 0 && (parsed.ok === true || parsedRoot.ok === true) && sourceReady; + return { + ok, + command: `hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`, + node: scoped.node, + lane: scoped.lane, + mode: "gitea-source+legacy-gitops-flush", + mutation: false, + namespace: record(legacy).namespace ?? null, + readUrl: scoped.spec.gitReadUrl, + writeUrl: scoped.spec.gitWriteUrl, + sourceBranch: scoped.spec.sourceBranch, + gitopsBranch: scoped.spec.gitopsBranch, + resources: record(legacy.resources), + githubTransport: record(legacy.githubTransport), + summary, + gitea: { + repoKey, + repository: repo, + status: parsed, + result: compactRuntimeCommand(result), + }, + legacyGitMirror: { + role: "gitops-flush-only", + ok: legacy.ok === true, + summary: legacySummary, + degradedReason: legacy.degradedReason ?? null, + }, + refSources: summary.refSources, + result: compactRuntimeCommand(result), + degradedReason: ok ? undefined : "node-runtime-gitea-source-snapshot-not-ready", + valuesPrinted: false, + next: { + sync: `bun scripts/cli.ts platform-infra gitea mirror sync --target ${scoped.node} --repo ${repoKey} --confirm`, + flush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm`, + }, + }; +} + +function nodeRuntimeLegacyGitMirrorStatus(scoped: ReturnType): Record { const spec = scoped.spec; const mirror = nodeRuntimeGitMirrorTarget(spec); const script = [ @@ -703,6 +808,7 @@ export function nodeRuntimeOpportunisticGitMirrorSync( pipelineRun: string, options: NodeRuntimeGitMirrorRunOptions = {}, ): Record | null { + if (scoped.spec.sourceAuthority?.mode === "giteaSnapshot") return null; const stage = "git-mirror-parallel-sync"; const syncScoped = nodeRuntimeScopedWithinDeadline(scoped, options.deadlineMs); if (syncScoped === null) return null; diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index a2c57435..ed3c7917 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -648,7 +648,7 @@ async function mirrorCommand(config: UniDeskConfig, args: string[]): Promise { }; } -async function mirrorStatus(config: UniDeskConfig, options: CommonOptions): Promise> { +async function mirrorStatus(config: UniDeskConfig, options: CommonOptions & { repoKey?: string | null }): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); const health = await validate(config, options); const healthValidation = record(health.validation); const credentials = credentialSummaries(gitea); const secrets = ensureMirrorSecrets(gitea, false); - const result = await capture(config, target.route, ["sh"], remoteScript("mirror-status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }, { repos: repositoriesForTarget(gitea, target), secrets })); + const selectedRepos = selectedRepositories(gitea, target, options.repoKey ?? null); + const result = await capture(config, target.route, ["sh"], remoteScript("mirror-status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }, { repos: selectedRepos, secrets })); const parsed = parseJsonOutput(result.stdout); const repositories = arrayRecords(record(parsed).repositories); return { @@ -711,7 +712,7 @@ async function mirrorStatus(config: UniDeskConfig, options: CommonOptions): Prom readinessDetail: "Gitea source-authority is ready when every configured repo has branch and snapshot refs in Gitea.", }, responsibilities: gitea.sourceAuthority.responsibilities.map(responsibilitySummary), - repositories: repositories.length > 0 ? repositories : repositoriesForTarget(gitea, target).map((repo) => repositorySummary(gitea, repo)), + repositories: repositories.length > 0 ? repositories : selectedRepos.map((repo) => repositorySummary(gitea, repo)), credentials, remote: parsed ?? compactCapture(result, { full: options.full || result.exitCode !== 0 }), next: mirrorNextCommands(target.id), diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index be58f9e9..f7c698ac 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -451,7 +451,7 @@ function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfi UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix, UNIDESK_PAC_ARGO_NAMESPACE: consumer.argoNamespace, UNIDESK_PAC_ARGO_APPLICATION: consumer.argoApplication, - UNIDESK_PAC_PART_OF: consumer.id.startsWith("sentinel") ? "hwlab-web-probe-sentinel" : "agentrun", + UNIDESK_PAC_PART_OF: pacPartOf(consumer.id), UNIDESK_PAC_SPEC: kubernetesLabelValue(pac.metadata.relatedIssues.includes(1555) ? "GH-1552-GH-1555" : pac.metadata.spec), }; const exports = Object.entries(env).map(([key, value]) => `export ${key}=${shQuote(value)}`).join("\n"); @@ -462,6 +462,12 @@ function credentialPath(root: string, sourceRef: string): string { return sourceRef.startsWith("/") ? sourceRef : join(root, sourceRef); } +function pacPartOf(consumerId: string): string { + if (consumerId.startsWith("sentinel")) return "hwlab-web-probe-sentinel"; + if (consumerId.startsWith("hwlab-")) return "hwlab"; + return "agentrun"; +} + function parseLinePair(path: string, usernameLine: number, passwordLine: number): { username: string; password: string } { const lines = readFileSync(path, "utf8").split(/\r?\n/u); const username = lines[usernameLine - 1]?.trim() ?? "";